diff --git a/assets/js/controllers/postedit.js b/assets/js/controllers/postedit.js index f800c44..71328e3 100644 --- a/assets/js/controllers/postedit.js +++ b/assets/js/controllers/postedit.js @@ -60,16 +60,6 @@ export default class PosteditController extends Controller { try { const formData = new FormData(this.element); let data = Object.fromEntries(formData.entries()); - - // Special handling for categories - let categoryIDs = []; - for (let i of formData.entries()) { - if (i[0] === "category_ids") { - categoryIDs.push(parseInt(i[1])) - } - } - - data["category_ids"] = categoryIDs; data = {...data, action: action || 'save'}; const response = await fetch(this.element.getAttribute("action"), { diff --git a/cmds/server.go b/cmds/server.go index 29a8c2a..28e2ccc 100644 --- a/cmds/server.go +++ b/cmds/server.go @@ -115,7 +115,6 @@ Starting weiro without any arguments will start the server. ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites} ch := handlers.CategoriesHandler{CategoryService: svcs.Categories} pgh := handlers.PagesHandler{PageService: svcs.Pages} - oih := handlers.ObsImportHandler{ObsImportService: svcs.ObsImport, ScratchDir: cfg.ScratchDir} app.Get("/login", lh.Login) app.Post("/login", lh.DoLogin) @@ -163,9 +162,6 @@ Starting weiro without any arguments will start the server. siteGroup.Get("/settings", ssh.General) siteGroup.Post("/settings", ssh.UpdateGeneral) - siteGroup.Get("/import/obsidian", oih.Form) - siteGroup.Post("/import/obsidian", oih.Upload) - siteGroup.Get("/categories", ch.Index) siteGroup.Get("/categories/new", ch.New) siteGroup.Get("/categories/:categoryID", ch.Edit) diff --git a/handlers/login.go b/handlers/login.go index 34c1e96..30ed0b4 100644 --- a/handlers/login.go +++ b/handlers/login.go @@ -37,8 +37,9 @@ func (lh *LoginHandler) Logout(c fiber.Ctx) error { func (lh *LoginHandler) DoLogin(c fiber.Ctx) error { var req struct { - Username string `form:"username"` - Password string `form:"password"` + Username string `form:"username"` + Password string `form:"password"` + LoginChallenge string `form:"_login_challenge"` } if err := c.Bind().Body(&req); err != nil { return c.Status(fiber.StatusBadRequest).SendString("Failed to parse request body") @@ -50,6 +51,11 @@ func (lh *LoginHandler) DoLogin(c fiber.Ctx) error { sess := session.FromContext(c) + challenge, _ := sess.Get("_login_challenge").(string) + if challenge != req.LoginChallenge { + return c.Redirect().To("/login") + } + user, err := lh.AuthService.Login(c.Context(), req.Username, req.Password) if err != nil { return c.Status(fiber.StatusInternalServerError).SendString("Failed to login") diff --git a/handlers/obsimport.go b/handlers/obsimport.go deleted file mode 100644 index e20be77..0000000 --- a/handlers/obsimport.go +++ /dev/null @@ -1,50 +0,0 @@ -package handlers - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/gofiber/fiber/v3" - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/services/obsimport" -) - -type ObsImportHandler struct { - ObsImportService *obsimport.Service - ScratchDir string -} - -func (h ObsImportHandler) Form(c fiber.Ctx) error { - return c.Render("obsimport/form", fiber.Map{}) -} - -func (h ObsImportHandler) Upload(c fiber.Ctx) error { - site := c.Locals("site").(models.Site) - - fileHeader, err := c.FormFile("zipfile") - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "no file provided") - } - - // Save uploaded file to scratch dir - if err := os.MkdirAll(h.ScratchDir, 0755); err != nil { - return err - } - - dstPath := filepath.Join(h.ScratchDir, models.NewNanoID()+".zip") - if err := c.SaveFile(fileHeader, dstPath); err != nil { - return err - } - defer os.Remove(dstPath) - - result, err := h.ObsImportService.ImportZip(c.Context(), dstPath) - if err != nil { - return err - } - - return c.Render("obsimport/result", fiber.Map{ - "result": result, - "siteURL": fmt.Sprintf("/sites/%v/posts", site.ID), - }) -} diff --git a/providers/db/categories.go b/providers/db/categories.go index 23a9e67..72fac94 100644 --- a/providers/db/categories.go +++ b/providers/db/categories.go @@ -82,8 +82,8 @@ func (db *Provider) SelectCategoriesOfPost(ctx context.Context, postID int64) ([ return cats, nil } -func (db *Provider) SelectPublishedPostsOfCategory(ctx context.Context, categoryID int64, pp PagingParams) ([]*models.Post, error) { - rows, err := db.queries.SelectPublishedPostsOfCategory(ctx, sqlgen.SelectPublishedPostsOfCategoryParams{ +func (db *Provider) SelectPostsOfCategory(ctx context.Context, categoryID int64, pp PagingParams) ([]*models.Post, error) { + rows, err := db.queries.SelectPostsOfCategory(ctx, sqlgen.SelectPostsOfCategoryParams{ CategoryID: categoryID, Limit: pp.Limit, Offset: pp.Offset, diff --git a/providers/db/gen/sqlgen/categories.sql.go b/providers/db/gen/sqlgen/categories.sql.go index f6a291f..95a26e5 100644 --- a/providers/db/gen/sqlgen/categories.sql.go +++ b/providers/db/gen/sqlgen/categories.sql.go @@ -227,7 +227,7 @@ func (q *Queries) SelectCategoryBySlugAndSite(ctx context.Context, arg SelectCat return i, err } -const selectPublishedPostsOfCategory = `-- name: SelectPublishedPostsOfCategory :many +const selectPostsOfCategory = `-- name: SelectPostsOfCategory :many SELECT p.id, p.site_id, p.state, p.guid, p.title, p.body, p.slug, p.created_at, p.updated_at, p.published_at, p.deleted_at FROM posts p INNER JOIN post_categories pc ON pc.post_id = p.id WHERE pc.category_id = ? AND p.state = 0 AND p.deleted_at = 0 @@ -235,14 +235,14 @@ ORDER BY p.published_at DESC LIMIT ? OFFSET ? ` -type SelectPublishedPostsOfCategoryParams struct { +type SelectPostsOfCategoryParams struct { CategoryID int64 Limit int64 Offset int64 } -func (q *Queries) SelectPublishedPostsOfCategory(ctx context.Context, arg SelectPublishedPostsOfCategoryParams) ([]Post, error) { - rows, err := q.db.QueryContext(ctx, selectPublishedPostsOfCategory, arg.CategoryID, arg.Limit, arg.Offset) +func (q *Queries) SelectPostsOfCategory(ctx context.Context, arg SelectPostsOfCategoryParams) ([]Post, error) { + rows, err := q.db.QueryContext(ctx, selectPostsOfCategory, arg.CategoryID, arg.Limit, arg.Offset) if err != nil { return nil, err } diff --git a/providers/db/gen/sqlgen/posts.sql.go b/providers/db/gen/sqlgen/posts.sql.go index b1d3afb..129a49a 100644 --- a/providers/db/gen/sqlgen/posts.sql.go +++ b/providers/db/gen/sqlgen/posts.sql.go @@ -200,54 +200,6 @@ func (q *Queries) SelectPostsOfSite(ctx context.Context, arg SelectPostsOfSitePa return items, nil } -const selectPublishedPostsOfSite = `-- name: SelectPublishedPostsOfSite :many -SELECT id, site_id, state, guid, title, body, slug, created_at, updated_at, published_at, deleted_at -FROM posts -WHERE site_id = ?1 AND state = 0 AND deleted_at = 0 -ORDER BY published_at DESC LIMIT ?3 OFFSET ?2 -` - -type SelectPublishedPostsOfSiteParams struct { - SiteID int64 - Offset int64 - Limit int64 -} - -func (q *Queries) SelectPublishedPostsOfSite(ctx context.Context, arg SelectPublishedPostsOfSiteParams) ([]Post, error) { - rows, err := q.db.QueryContext(ctx, selectPublishedPostsOfSite, arg.SiteID, arg.Offset, arg.Limit) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Post - for rows.Next() { - var i Post - if err := rows.Scan( - &i.ID, - &i.SiteID, - &i.State, - &i.Guid, - &i.Title, - &i.Body, - &i.Slug, - &i.CreatedAt, - &i.UpdatedAt, - &i.PublishedAt, - &i.DeletedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const softDeletePost = `-- name: SoftDeletePost :exec UPDATE posts SET deleted_at = ? WHERE id = ? ` diff --git a/providers/db/posts.go b/providers/db/posts.go index 3b86aaf..7f58d1a 100644 --- a/providers/db/posts.go +++ b/providers/db/posts.go @@ -47,23 +47,6 @@ func (db *Provider) SelectPostsOfSite(ctx context.Context, siteID int64, showDel return posts, nil } -func (db *Provider) SelectPublishedPostsOfSite(ctx context.Context, siteID int64, pp PagingParams) ([]*models.Post, error) { - rows, err := db.queries.SelectPublishedPostsOfSite(ctx, sqlgen.SelectPublishedPostsOfSiteParams{ - SiteID: siteID, - Limit: pp.Limit, - Offset: pp.Offset, - }) - if err != nil { - return nil, err - } - - posts := make([]*models.Post, len(rows)) - for i, row := range rows { - posts[i] = dbPostToPost(row) - } - return posts, nil -} - func (db *Provider) SelectPost(ctx context.Context, postID int64) (*models.Post, error) { row, err := db.queries.SelectPost(ctx, postID) if err != nil { diff --git a/providers/markdown/renderer.go b/providers/markdown/renderer.go index 828ba96..aedd184 100644 --- a/providers/markdown/renderer.go +++ b/providers/markdown/renderer.go @@ -22,7 +22,7 @@ type Renderer struct { func NewRendererForUI() *Renderer { mdParser := goldmark.New( - goldmark.WithExtensions(extension.GFM, extension.Footnote), + goldmark.WithExtensions(extension.GFM), goldmark.WithRendererOptions( gm_html.WithUnsafe(), ), @@ -48,7 +48,7 @@ func NewRendererForUI() *Renderer { func NewRendererForSite() *Renderer { mdParser := goldmark.New( - goldmark.WithExtensions(extension.GFM, extension.Footnote), + goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), diff --git a/providers/sitebuilder/builder.go b/providers/sitebuilder/builder.go index d0bf17b..71ce926 100644 --- a/providers/sitebuilder/builder.go +++ b/providers/sitebuilder/builder.go @@ -49,7 +49,6 @@ func New(site pubmodel.Site, opts Options) (*Builder, error) { mdRenderer: markdown.NewRendererForSite(), postMDProcessors: []postMDProcessor{ uploadAbsoluteURL, - removeFootnoteHRs, }, }, nil } diff --git a/providers/sitebuilder/processors.go b/providers/sitebuilder/processors.go index 605d077..c699160 100644 --- a/providers/sitebuilder/processors.go +++ b/providers/sitebuilder/processors.go @@ -35,8 +35,3 @@ func uploadAbsoluteURL(site pubmodel.Site, dom *goquery.Document) error { }) return nil } - -func removeFootnoteHRs(site pubmodel.Site, dom *goquery.Document) error { - dom.Find("div.footnotes > hr").Remove() - return nil -} diff --git a/services/obsimport/service.go b/services/obsimport/service.go deleted file mode 100644 index 0852031..0000000 --- a/services/obsimport/service.go +++ /dev/null @@ -1,229 +0,0 @@ -package obsimport - -import ( - "archive/zip" - "bufio" - "context" - "fmt" - "io" - "log" - "mime" - "os" - "path/filepath" - "strings" - "time" - - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/providers/db" - "lmika.dev/lmika/weiro/providers/uploadfiles" - "lmika.dev/lmika/weiro/services/publisher" -) - -type Service struct { - db *db.Provider - up *uploadfiles.Provider - publisher *publisher.Queue - scratchDir string -} - -func New(db *db.Provider, up *uploadfiles.Provider, publisher *publisher.Queue, scratchDir string) *Service { - return &Service{ - db: db, - up: up, - publisher: publisher, - scratchDir: scratchDir, - } -} - -type ImportResult struct { - PostsImported int - UploadsImported int -} - -func (s *Service) ImportZip(ctx context.Context, zipPath string) (ImportResult, error) { - site, ok := models.GetSite(ctx) - if !ok { - return ImportResult{}, models.SiteRequiredError - } - - zr, err := zip.OpenReader(zipPath) - if err != nil { - return ImportResult{}, fmt.Errorf("open zip: %w", err) - } - defer zr.Close() - - var result ImportResult - - for _, f := range zr.File { - if f.FileInfo().IsDir() { - continue - } - - ext := strings.ToLower(filepath.Ext(f.Name)) - if ext == ".md" || ext == ".markdown" { - if err := s.importNote(ctx, site, f); err != nil { - log.Printf("warn: skipping note %s: %v", f.Name, err) - continue - } - result.PostsImported++ - } else if isAttachment(ext) { - if err := s.importAttachment(ctx, site, f); err != nil { - log.Printf("warn: skipping attachment %s: %v", f.Name, err) - continue - } - result.UploadsImported++ - } - } - - s.publisher.Queue(site) - - return result, nil -} - -func (s *Service) importNote(ctx context.Context, site models.Site, f *zip.File) error { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - - data, err := io.ReadAll(rc) - if err != nil { - return err - } - - body := stripFrontMatter(string(data)) - title := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)) - publishedAt := f.Modified - if publishedAt.IsZero() { - publishedAt = time.Now() - } - - renderTZ, err := time.LoadLocation(site.Timezone) - if err != nil { - renderTZ = time.UTC - } - publishedAt = publishedAt.In(renderTZ) - - post := &models.Post{ - SiteID: site.ID, - GUID: models.NewNanoID(), - State: models.StatePublished, - Title: title, - Body: body, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - PublishedAt: publishedAt, - } - post.Slug = post.BestSlug() - - return s.db.SavePost(ctx, post) -} - -func (s *Service) importAttachment(ctx context.Context, site models.Site, f *zip.File) error { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - - // Write to a temp file in scratch dir - if err := os.MkdirAll(s.scratchDir, 0755); err != nil { - return err - } - - tmpFile, err := os.CreateTemp(s.scratchDir, "obsimport-*"+filepath.Ext(f.Name)) - if err != nil { - return err - } - tmpPath := tmpFile.Name() - - if _, err := io.Copy(tmpFile, rc); err != nil { - tmpFile.Close() - os.Remove(tmpPath) - return err - } - tmpFile.Close() - - filename := filepath.Base(f.Name) - mimeType := mime.TypeByExtension(filepath.Ext(filename)) - if mimeType == "" { - mimeType = "application/octet-stream" - } - - stat, err := os.Stat(tmpPath) - if err != nil { - os.Remove(tmpPath) - return err - } - - newUploadGUID := models.NewNanoID() - newTime := time.Now().UTC() - newSlug := filepath.Join( - fmt.Sprintf("%04d", newTime.Year()), - fmt.Sprintf("%02d", newTime.Month()), - newUploadGUID+filepath.Ext(filename), - ) - - newUpload := models.Upload{ - SiteID: site.ID, - GUID: models.NewNanoID(), - FileSize: stat.Size(), - MIMEType: mimeType, - Filename: filename, - CreatedAt: newTime, - Slug: newSlug, - } - if err := s.db.SaveUpload(ctx, &newUpload); err != nil { - os.Remove(tmpPath) - return err - } - - if err := s.up.AdoptFile(site, newUpload, tmpPath); err != nil { - os.Remove(tmpPath) - return err - } - - return nil -} - -// stripFrontMatter removes YAML front matter (delimited by ---) from markdown content. -func stripFrontMatter(content string) string { - scanner := bufio.NewScanner(strings.NewReader(content)) - - // Check if the first line is a front matter delimiter - if !scanner.Scan() { - return content - } - firstLine := strings.TrimSpace(scanner.Text()) - if firstLine != "---" { - return content - } - - // Skip until the closing --- - for scanner.Scan() { - if strings.TrimSpace(scanner.Text()) == "---" { - // Return everything after the closing delimiter - var rest strings.Builder - for scanner.Scan() { - rest.WriteString(scanner.Text()) - rest.WriteString("\n") - } - return strings.TrimLeft(rest.String(), "\n") - } - } - - // No closing delimiter found, return original content - return content -} - -var attachmentExts = map[string]bool{ - ".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".svg": true, ".webp": true, - ".bmp": true, ".ico": true, ".tiff": true, ".tif": true, - ".mp3": true, ".mp4": true, ".wav": true, ".ogg": true, ".webm": true, - ".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true, -} - -func isAttachment(ext string) bool { - return attachmentExts[ext] -} diff --git a/services/obsimport/service_test.go b/services/obsimport/service_test.go deleted file mode 100644 index 51123de..0000000 --- a/services/obsimport/service_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package obsimport - -import "testing" - -func TestStripFrontMatter(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - { - name: "no front matter", - input: "Hello world\nThis is a note", - want: "Hello world\nThis is a note", - }, - { - name: "with front matter", - input: "---\ntitle: Test\ntags: [a, b]\n---\nHello world\nThis is a note\n", - want: "Hello world\nThis is a note\n", - }, - { - name: "only front matter", - input: "---\ntitle: Test\n---\n", - want: "", - }, - { - name: "unclosed front matter", - input: "---\ntitle: Test\nno closing delimiter", - want: "---\ntitle: Test\nno closing delimiter", - }, - { - name: "empty string", - input: "", - want: "", - }, - { - name: "front matter with leading newlines stripped", - input: "---\nkey: val\n---\n\n\nBody here\n", - want: "Body here\n", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := stripFrontMatter(tt.input) - if got != tt.want { - t.Errorf("stripFrontMatter() = %q, want %q", got, tt.want) - } - }) - } -} diff --git a/services/publisher/iter.go b/services/publisher/iter.go index d07d4fe..ea70616 100644 --- a/services/publisher/iter.go +++ b/services/publisher/iter.go @@ -9,10 +9,10 @@ import ( ) // postIter returns a post iterator which returns posts in reverse chronological order. -func (s *Publisher) publishedPostIter(ctx context.Context, site int64) iter.Seq[models.Maybe[*models.Post]] { +func (s *Publisher) postIter(ctx context.Context, site int64) iter.Seq[models.Maybe[*models.Post]] { return func(yield func(models.Maybe[*models.Post]) bool) { paging := db.PagingParams{Offset: 0, Limit: 50} - page, err := s.db.SelectPublishedPostsOfSite(ctx, site, paging) + page, err := s.db.SelectPostsOfSite(ctx, site, false, paging) if err != nil { yield(models.Maybe[*models.Post]{Err: err}) return @@ -45,7 +45,7 @@ func (s *Publisher) postIterByCategory(ctx context.Context, categoryID int64) it return func(yield func(models.Maybe[*models.Post]) bool) { paging := db.PagingParams{Offset: 0, Limit: 50} for { - page, err := s.db.SelectPublishedPostsOfCategory(ctx, categoryID, paging) + page, err := s.db.SelectPostsOfCategory(ctx, categoryID, paging) if err != nil { yield(models.Maybe[*models.Post]{Err: err}) return diff --git a/services/publisher/service.go b/services/publisher/service.go index a5072a5..adfcdd7 100644 --- a/services/publisher/service.go +++ b/services/publisher/service.go @@ -79,7 +79,7 @@ func (p *Publisher) Publish(ctx context.Context, site models.Site) error { pubSite := pubmodel.Site{ Site: site, PostIter: func(ctx context.Context) iter.Seq[models.Maybe[*models.Post]] { - return p.publishedPostIter(ctx, site.ID) + return p.postIter(ctx, site.ID) }, BaseURL: target.BaseURL, Uploads: uploads, diff --git a/services/services.go b/services/services.go index a79e903..ab1a4ca 100644 --- a/services/services.go +++ b/services/services.go @@ -9,7 +9,6 @@ import ( "lmika.dev/lmika/weiro/services/auth" "lmika.dev/lmika/weiro/services/categories" "lmika.dev/lmika/weiro/services/imgedit" - "lmika.dev/lmika/weiro/services/obsimport" "lmika.dev/lmika/weiro/services/pages" "lmika.dev/lmika/weiro/services/posts" "lmika.dev/lmika/weiro/services/publisher" @@ -28,7 +27,6 @@ type Services struct { ImageEdit *imgedit.Service Categories *categories.Service Pages *pages.Service - ObsImport *obsimport.Service } func New(cfg config.Config) (*Services, error) { @@ -48,7 +46,6 @@ func New(cfg config.Config) (*Services, error) { imageEditService := imgedit.New(uploadService, filepath.Join(cfg.ScratchDir, "imageedit")) categoriesService := categories.New(dbp, publisherQueue) pagesService := pages.New(dbp, publisherQueue) - obsImportService := obsimport.New(dbp, ufp, publisherQueue, filepath.Join(cfg.ScratchDir, "obsimport")) return &Services{ DB: dbp, @@ -61,7 +58,6 @@ func New(cfg config.Config) (*Services, error) { ImageEdit: imageEditService, Categories: categoriesService, Pages: pagesService, - ObsImport: obsImportService, }, nil } diff --git a/sql/queries/categories.sql b/sql/queries/categories.sql index b8e0e64..4b48506 100644 --- a/sql/queries/categories.sql +++ b/sql/queries/categories.sql @@ -17,7 +17,7 @@ INNER JOIN post_categories pc ON pc.category_id = c.id WHERE pc.post_id = ? ORDER BY c.name ASC; --- name: SelectPublishedPostsOfCategory :many +-- name: SelectPostsOfCategory :many SELECT p.* FROM posts p INNER JOIN post_categories pc ON pc.post_id = p.id WHERE pc.category_id = ? AND p.state = 0 AND p.deleted_at = 0 diff --git a/sql/queries/posts.sql b/sql/queries/posts.sql index feaae7f..5a4c18e 100644 --- a/sql/queries/posts.sql +++ b/sql/queries/posts.sql @@ -17,12 +17,6 @@ WHERE site_id = sqlc.arg(site_id) AND ( END ) ORDER BY created_at DESC LIMIT sqlc.arg(limit) OFFSET sqlc.arg(offset); --- name: SelectPublishedPostsOfSite :many -SELECT * -FROM posts -WHERE site_id = sqlc.arg(site_id) AND state = 0 AND deleted_at = 0 -ORDER BY published_at DESC LIMIT sqlc.arg(limit) OFFSET sqlc.arg(offset); - -- name: SelectPost :one SELECT * FROM posts WHERE id = ? LIMIT 1; diff --git a/views/obsimport/form.html b/views/obsimport/form.html deleted file mode 100644 index ccb27a5..0000000 --- a/views/obsimport/form.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
-
Import from Obsidian
-

Select an Obsidian vault exported as a Zip file. All Markdown notes will be imported as posts, and any images or attachments will be imported as uploads.

-
-
- -
- -
-
-
-
-
- - Cancel -
-
-
-
-
diff --git a/views/obsimport/result.html b/views/obsimport/result.html deleted file mode 100644 index 15ebe31..0000000 --- a/views/obsimport/result.html +++ /dev/null @@ -1,10 +0,0 @@ -
-
-
Import Complete
-
-

Successfully imported {{ .result.PostsImported }} post(s) and {{ .result.UploadsImported }} upload(s).

-
- Go to Posts - Back to Settings -
-
diff --git a/views/sitesettings/general.html b/views/sitesettings/general.html index c0989c5..6f1833b 100644 --- a/views/sitesettings/general.html +++ b/views/sitesettings/general.html @@ -66,12 +66,5 @@ -
-
-
- Import Obsidian - Import posts and attachments from an Obsidian vault zip file. -
-
\ No newline at end of file