diff --git a/assets/js/controllers/pagelist.js b/assets/js/controllers/pagelist.js deleted file mode 100644 index 7da6872..0000000 --- a/assets/js/controllers/pagelist.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Controller } from "@hotwired/stimulus" -import { showToast } from "../services/toast"; - -export default class PagelistController extends Controller { - static values = { - siteId: Number, - }; - - static targets = ["list"]; - - dragStart(ev) { - this.draggedRow = ev.currentTarget; - ev.currentTarget.classList.add("opacity-50"); - ev.dataTransfer.effectAllowed = "move"; - } - - dragOver(ev) { - ev.preventDefault(); - ev.dataTransfer.dropEffect = "move"; - } - - drop(ev) { - ev.preventDefault(); - const targetRow = ev.currentTarget; - if (this.draggedRow && this.draggedRow !== targetRow) { - const rows = [...this.listTarget.children]; - const draggedIdx = rows.indexOf(this.draggedRow); - const targetIdx = rows.indexOf(targetRow); - if (draggedIdx < targetIdx) { - targetRow.after(this.draggedRow); - } else { - targetRow.before(this.draggedRow); - } - this.saveOrder(); - } - } - - dragEnd(ev) { - ev.currentTarget.classList.remove("opacity-50"); - this.draggedRow = null; - } - - async saveOrder() { - const rows = [...this.listTarget.children]; - const pageIds = rows.map(row => parseInt(row.dataset.pageId, 10)); - - try { - await fetch(`/sites/${this.siteIdValue}/pages/reorder`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - body: JSON.stringify({ page_ids: pageIds }), - }); - } catch (error) { - showToast({ - title: "Error", - body: "Failed to reorder pages.", - }); - } - } -} diff --git a/assets/js/main.js b/assets/js/main.js index 28451fb..d76c353 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -7,7 +7,6 @@ import LogoutController from "./controllers/logout"; import FirstRunController from "./controllers/firstrun"; import UploadController from "./controllers/upload"; import ShowUploadController from "./controllers/show_upload"; -import PagelistController from "./controllers/pagelist"; window.Stimulus = Application.start() Stimulus.register("toast", ToastController); @@ -16,5 +15,4 @@ Stimulus.register("postedit", PosteditController); Stimulus.register("logout", LogoutController); Stimulus.register("first-run", FirstRunController); Stimulus.register("upload", UploadController); -Stimulus.register("show-upload", ShowUploadController); -Stimulus.register("pagelist", PagelistController); \ No newline at end of file +Stimulus.register("show-upload", ShowUploadController); \ No newline at end of file diff --git a/cmds/server.go b/cmds/server.go index 89310bd..56517e7 100644 --- a/cmds/server.go +++ b/cmds/server.go @@ -113,7 +113,6 @@ Starting weiro without any arguments will start the server. uh := handlers.UploadsHandler{UploadsService: svcs.Uploads} ssh := handlers.SiteSettingsHandler{SiteService: svcs.Sites} ch := handlers.CategoriesHandler{CategoryService: svcs.Categories} - pgh := handlers.PagesHandler{PageService: svcs.Pages} app.Get("/login", lh.Login) app.Post("/login", lh.DoLogin) @@ -150,14 +149,6 @@ Starting weiro without any arguments will start the server. siteGroup.Post("/categories/:categoryID", ch.Update) siteGroup.Post("/categories/:categoryID/delete", ch.Delete) - siteGroup.Get("/pages", pgh.Index) - siteGroup.Get("/pages/new", pgh.New) - siteGroup.Get("/pages/:pageID", pgh.Edit) - siteGroup.Post("/pages", pgh.Create) - siteGroup.Post("/pages/reorder", pgh.Reorder) - siteGroup.Post("/pages/:pageID", pgh.Update) - siteGroup.Post("/pages/:pageID/delete", pgh.Delete) - app.Get("/", middleware.OptionalUser(svcs.Auth), ih.Index) app.Get("/first-run", ih.FirstRun) app.Post("/first-run", ih.FirstRunSubmit) diff --git a/docs/superpowers/plans/2026-03-22-pages.md b/docs/superpowers/plans/2026-03-22-pages.md deleted file mode 100644 index 89a3983..0000000 --- a/docs/superpowers/plans/2026-03-22-pages.md +++ /dev/null @@ -1,1218 +0,0 @@ -# Arbitrary Pages Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow users to create arbitrary pages with title, slug, markdown body, page type, nav visibility, and sort order, rendered on the generated site. - -**Architecture:** New `pages` table + model + service + handler + admin views following the existing categories pattern. Publisher populates `pubmodel.Site.Pages`, and the site builder renders pages **after** all other content so conflicting slugs silently override auto-generated files. Drag-and-drop reordering in admin via a new Stimulus controller. - -**Tech Stack:** Go/Fiber v3, SQLite/sqlc, Bootstrap 5, Stimulus.js, goldmark markdown, html/template - ---- - -## File Structure - -**New files:** -- `sql/schema/06_pages.up.sql` - Migration for pages table -- `sql/queries/pages.sql` - sqlc queries for pages -- `models/pages.go` - Page model struct and slug helper -- `providers/db/pages.go` - DB provider methods for pages -- `services/pages/service.go` - Pages service layer -- `handlers/pages.go` - Admin pages handler -- `views/pages/index.html` - Admin page list with drag-and-drop -- `views/pages/edit.html` - Admin page edit form (two-column) -- `assets/js/controllers/pagelist.js` - Stimulus controller for drag-and-drop reorder -- `layouts/simplecss/templates/pages_single.html` - Generated site page template -- `providers/sitebuilder/render_pages.go` - Builder renderPages method - -**Modified files:** -- `providers/db/gen/sqlgen/` - Regenerated sqlc output -- `models/pubmodel/sites.go` - Add `Pages []models.Page` field -- `services/publisher/service.go` - Fetch pages and populate pubmodel -- `providers/sitebuilder/tmpls.go` - Add pageSingleData type and template constant -- `providers/sitebuilder/builder.go` - Call renderPages after eg.Wait() -- `providers/sitebuilder/builder_test.go` - Add pages to test -- `views/_common/nav.html` - Add "Pages" nav item -- `services/services.go` - Wire up pages service -- `cmds/server.go` - Wire up pages handler and routes -- `assets/js/main.js` - Register pagelist controller -- `esbuild.mjs` - No change needed (auto-picks up new JS files) - ---- - -### Task 1: Schema Migration and sqlc Queries - -**Files:** -- Create: `sql/schema/06_pages.up.sql` -- Create: `sql/queries/pages.sql` -- Regenerate: `providers/db/gen/sqlgen/` - -- [ ] **Step 1: Write the schema migration** - -```sql --- sql/schema/06_pages.up.sql -CREATE TABLE pages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - site_id INTEGER NOT NULL, - guid TEXT NOT NULL, - title TEXT NOT NULL, - slug TEXT NOT NULL, - body TEXT NOT NULL, - page_type INTEGER NOT NULL DEFAULT 0, - show_in_nav INTEGER NOT NULL DEFAULT 0, - sort_order INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE -); -CREATE INDEX idx_pages_site ON pages (site_id); -CREATE UNIQUE INDEX idx_pages_guid ON pages (guid); -CREATE UNIQUE INDEX idx_pages_site_slug ON pages (site_id, slug); -``` - -- [ ] **Step 2: Write the sqlc queries** - -```sql --- sql/queries/pages.sql - --- name: SelectPagesOfSite :many -SELECT * FROM pages -WHERE site_id = ? ORDER BY sort_order ASC; - --- name: SelectPage :one -SELECT * FROM pages WHERE id = ? LIMIT 1; - --- name: SelectPageByGUID :one -SELECT * FROM pages WHERE guid = ? LIMIT 1; - --- name: SelectPageBySlugAndSite :one -SELECT * FROM pages WHERE site_id = ? AND slug = ? LIMIT 1; - --- name: InsertPage :one -INSERT INTO pages ( - site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id; - --- name: UpdatePage :exec -UPDATE pages SET - title = ?, - slug = ?, - body = ?, - page_type = ?, - show_in_nav = ?, - updated_at = ? -WHERE id = ?; - --- name: UpdatePageSortOrder :exec -UPDATE pages SET sort_order = ? WHERE id = ?; - --- name: DeletePage :exec -DELETE FROM pages WHERE id = ?; -``` - -- [ ] **Step 3: Regenerate sqlc** - -Run: `sqlc generate` -Expected: Clean generation, new files in `providers/db/gen/sqlgen/` for pages queries. - -- [ ] **Step 4: Commit** - -```bash -git add sql/ providers/db/gen/ -git commit -m "feat(pages): add pages table schema and sqlc queries" -``` - ---- - -### Task 2: Page Model - -**Files:** -- Create: `models/pages.go` - -- [ ] **Step 1: Write the Page model and constants** - -```go -// models/pages.go -package models - -import ( - "strings" - "time" - "unicode" -) - -const ( - PageTypeNormal = 0 -) - -type Page struct { - ID int64 `json:"id"` - SiteID int64 `json:"site_id"` - GUID string `json:"guid"` - Title string `json:"title"` - Slug string `json:"slug"` - Body string `json:"body"` - PageType int `json:"page_type"` - ShowInNav bool `json:"show_in_nav"` - SortOrder int `json:"sort_order"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// GeneratePageSlug creates a URL-safe slug from a page title. -// e.g. "About Me" -> "about-me" -func GeneratePageSlug(title string) string { - var sb strings.Builder - prevDash := false - for _, c := range strings.TrimSpace(title) { - if unicode.IsLetter(c) || unicode.IsNumber(c) { - sb.WriteRune(unicode.ToLower(c)) - prevDash = false - } else if unicode.IsSpace(c) || c == '-' || c == '_' { - if !prevDash && sb.Len() > 0 { - sb.WriteRune('-') - prevDash = true - } - } - } - result := sb.String() - return strings.TrimRight(result, "-") -} -``` - -- [ ] **Step 2: Write a test for GeneratePageSlug** - -```go -// models/pages_test.go -package models_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "lmika.dev/lmika/weiro/models" -) - -func TestGeneratePageSlug(t *testing.T) { - tests := []struct { - title string - want string - }{ - {"About Me", "about-me"}, - {" Contact Us ", "contact-us"}, - {"Hello---World", "hello-world"}, - {"FAQ", "faq"}, - {"", ""}, - } - for _, tt := range tests { - t.Run(tt.title, func(t *testing.T) { - assert.Equal(t, tt.want, models.GeneratePageSlug(tt.title)) - }) - } -} -``` - -- [ ] **Step 3: Run tests** - -Run: `go test ./models/ -run TestGeneratePageSlug -v` -Expected: PASS - -- [ ] **Step 4: Commit** - -```bash -git add models/pages.go models/pages_test.go -git commit -m "feat(pages): add Page model and slug generator" -``` - ---- - -### Task 3: DB Provider for Pages - -**Files:** -- Create: `providers/db/pages.go` - -- [ ] **Step 1: Write the DB provider methods** - -Follow the pattern from `providers/db/categories.go`. The conversion function maps sqlgen types to model types. `ShowInNav` maps from `int64` (0/1) to `bool`. Timestamps map via `time.Unix(row.CreatedAt, 0).UTC()`. - -```go -// providers/db/pages.go -package db - -import ( - "context" - "time" - - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" -) - -func (db *Provider) SelectPagesOfSite(ctx context.Context, siteID int64) ([]*models.Page, error) { - rows, err := db.queries.SelectPagesOfSite(ctx, siteID) - if err != nil { - return nil, err - } - pages := make([]*models.Page, len(rows)) - for i, row := range rows { - pages[i] = dbPageToPage(row) - } - return pages, nil -} - -func (db *Provider) SelectPage(ctx context.Context, id int64) (*models.Page, error) { - row, err := db.queries.SelectPage(ctx, id) - if err != nil { - return nil, err - } - return dbPageToPage(row), nil -} - -func (db *Provider) SelectPageByGUID(ctx context.Context, guid string) (*models.Page, error) { - row, err := db.queries.SelectPageByGUID(ctx, guid) - if err != nil { - return nil, err - } - return dbPageToPage(row), nil -} - -func (db *Provider) SelectPageBySlugAndSite(ctx context.Context, siteID int64, slug string) (*models.Page, error) { - row, err := db.queries.SelectPageBySlugAndSite(ctx, sqlgen.SelectPageBySlugAndSiteParams{ - SiteID: siteID, - Slug: slug, - }) - if err != nil { - return nil, err - } - return dbPageToPage(row), nil -} - -func (db *Provider) SavePage(ctx context.Context, page *models.Page) error { - if page.ID == 0 { - showInNav := int64(0) - if page.ShowInNav { - showInNav = 1 - } - newID, err := db.queries.InsertPage(ctx, sqlgen.InsertPageParams{ - SiteID: page.SiteID, - Guid: page.GUID, - Title: page.Title, - Slug: page.Slug, - Body: page.Body, - PageType: int64(page.PageType), - ShowInNav: showInNav, - SortOrder: int64(page.SortOrder), - CreatedAt: timeToInt(page.CreatedAt), - UpdatedAt: timeToInt(page.UpdatedAt), - }) - if err != nil { - return err - } - page.ID = newID - return nil - } - - showInNav := int64(0) - if page.ShowInNav { - showInNav = 1 - } - return db.queries.UpdatePage(ctx, sqlgen.UpdatePageParams{ - Title: page.Title, - Slug: page.Slug, - Body: page.Body, - PageType: int64(page.PageType), - ShowInNav: showInNav, - UpdatedAt: timeToInt(page.UpdatedAt), - ID: page.ID, - }) -} - -func (db *Provider) UpdatePageSortOrder(ctx context.Context, id int64, sortOrder int) error { - return db.queries.UpdatePageSortOrder(ctx, sqlgen.UpdatePageSortOrderParams{ - SortOrder: int64(sortOrder), - ID: id, - }) -} - -func (db *Provider) DeletePage(ctx context.Context, id int64) error { - return db.queries.DeletePage(ctx, id) -} - -func dbPageToPage(row sqlgen.Page) *models.Page { - return &models.Page{ - ID: row.ID, - SiteID: row.SiteID, - GUID: row.Guid, - Title: row.Title, - Slug: row.Slug, - Body: row.Body, - PageType: int(row.PageType), - ShowInNav: row.ShowInNav != 0, - SortOrder: int(row.SortOrder), - CreatedAt: time.Unix(row.CreatedAt, 0).UTC(), - UpdatedAt: time.Unix(row.UpdatedAt, 0).UTC(), - } -} -``` - -**Important:** The exact field names on `sqlgen.InsertPageParams`, `sqlgen.UpdatePageParams`, etc. depend on what sqlc generates. Check the generated code in `providers/db/gen/sqlgen/pages.sql.go` to confirm field names and types before writing this file. Adjust as needed. - -- [ ] **Step 2: Verify it compiles** - -Run: `go build ./providers/db/...` -Expected: Clean compile - -- [ ] **Step 3: Commit** - -```bash -git add providers/db/pages.go -git commit -m "feat(pages): add DB provider methods for pages" -``` - ---- - -### Task 4: Pages Service - -**Files:** -- Create: `services/pages/service.go` -- Modify: `services/services.go` - -- [ ] **Step 1: Write the pages service** - -Follow the pattern from `services/categories/service.go`. The service gets site from context, validates ownership, generates slugs, and queues republish on mutations. - -```go -// services/pages/service.go -package pages - -import ( - "context" - "time" - - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/providers/db" - "lmika.dev/lmika/weiro/services/publisher" -) - -type CreatePageParams struct { - GUID string `form:"guid" json:"guid"` - Title string `form:"title" json:"title"` - Slug string `form:"slug" json:"slug"` - Body string `form:"body" json:"body"` - PageType int `form:"page_type" json:"page_type"` - ShowInNav bool `form:"show_in_nav" json:"show_in_nav"` -} - -type Service struct { - db *db.Provider - publisher *publisher.Queue -} - -func New(db *db.Provider, publisher *publisher.Queue) *Service { - return &Service{db: db, publisher: publisher} -} - -func (s *Service) ListPages(ctx context.Context) ([]*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - return s.db.SelectPagesOfSite(ctx, site.ID) -} - -func (s *Service) GetPage(ctx context.Context, id int64) (*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return nil, err - } - if page.SiteID != site.ID { - return nil, models.NotFoundError - } - return page, nil -} - -func (s *Service) CreatePage(ctx context.Context, params CreatePageParams) (*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - - now := time.Now() - slug := params.Slug - if slug == "" { - slug = models.GeneratePageSlug(params.Title) - } - - // Check slug collision - if _, err := s.db.SelectPageBySlugAndSite(ctx, site.ID, slug); err == nil { - return nil, models.SlugConflictError - } else if !db.ErrorIsNoRows(err) { - return nil, err - } - - // Determine sort order: place at end - existingPages, err := s.db.SelectPagesOfSite(ctx, site.ID) - if err != nil { - return nil, err - } - sortOrder := len(existingPages) - - page := &models.Page{ - SiteID: site.ID, - GUID: params.GUID, - Title: params.Title, - Slug: slug, - Body: params.Body, - PageType: params.PageType, - ShowInNav: params.ShowInNav, - SortOrder: sortOrder, - CreatedAt: now, - UpdatedAt: now, - } - if page.GUID == "" { - page.GUID = models.NewNanoID() - } - - if err := s.db.SavePage(ctx, page); err != nil { - return nil, err - } - - s.publisher.Queue(site) - return page, nil -} - -func (s *Service) UpdatePage(ctx context.Context, id int64, params CreatePageParams) (*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return nil, err - } - if page.SiteID != site.ID { - return nil, models.NotFoundError - } - - slug := params.Slug - if slug == "" { - slug = models.GeneratePageSlug(params.Title) - } - - // Check slug collision (exclude self) - if existing, err := s.db.SelectPageBySlugAndSite(ctx, site.ID, slug); err == nil && existing.ID != page.ID { - return nil, models.SlugConflictError - } else if err != nil && !db.ErrorIsNoRows(err) { - return nil, err - } - - page.Title = params.Title - page.Slug = slug - page.Body = params.Body - page.PageType = params.PageType - page.ShowInNav = params.ShowInNav - page.UpdatedAt = time.Now() - - if err := s.db.SavePage(ctx, page); err != nil { - return nil, err - } - - s.publisher.Queue(site) - return page, nil -} - -func (s *Service) DeletePage(ctx context.Context, id int64) error { - site, ok := models.GetSite(ctx) - if !ok { - return models.SiteRequiredError - } - - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return err - } - if page.SiteID != site.ID { - return models.NotFoundError - } - - if err := s.db.DeletePage(ctx, id); err != nil { - return err - } - - s.publisher.Queue(site) - return nil -} - -func (s *Service) ReorderPages(ctx context.Context, pageIDs []int64) error { - site, ok := models.GetSite(ctx) - if !ok { - return models.SiteRequiredError - } - - // Verify all pages belong to this site - for i, id := range pageIDs { - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return err - } - if page.SiteID != site.ID { - return models.NotFoundError - } - if err := s.db.UpdatePageSortOrder(ctx, id, i); err != nil { - return err - } - } - - s.publisher.Queue(site) - return nil -} -``` - -- [ ] **Step 2: Make SlugConflictError generic** - -In `models/errors.go`, change: -```go -var SlugConflictError = errors.New("a category with this slug already exists") -``` -To: -```go -var SlugConflictError = errors.New("a record with this slug already exists") -``` - -- [ ] **Step 3: Wire up the service in services/services.go** - -Add to the `Services` struct: -```go -Pages *pages.Service -``` - -Add to `New()`: -```go -pagesService := pages.New(dbp, publisherQueue) -``` - -And include in the return struct: -```go -Pages: pagesService, -``` - -Add import: `"lmika.dev/lmika/weiro/services/pages"` - -- [ ] **Step 4: Verify it compiles** - -Run: `go build ./services/...` -Expected: Clean compile - -- [ ] **Step 5: Commit** - -```bash -git add services/pages/ services/services.go models/errors.go -git commit -m "feat(pages): add pages service layer" -``` - ---- - -### Task 5: Pages Handler and Routes - -**Files:** -- Create: `handlers/pages.go` -- Modify: `cmds/server.go` - -- [ ] **Step 1: Write the pages handler** - -Follow the pattern from `handlers/categories.go` for CRUD, plus a `Reorder` handler that accepts JSON. - -```go -// handlers/pages.go -package handlers - -import ( - "fmt" - "strconv" - - "github.com/gofiber/fiber/v3" - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/services/pages" -) - -type PagesHandler struct { - PageService *pages.Service -} - -func (ph PagesHandler) Index(c fiber.Ctx) error { - pagesList, err := ph.PageService.ListPages(c.Context()) - if err != nil { - return err - } - - return c.Render("pages/index", fiber.Map{ - "pages": pagesList, - }) -} - -func (ph PagesHandler) New(c fiber.Ctx) error { - page := models.Page{ - GUID: models.NewNanoID(), - } - return c.Render("pages/edit", fiber.Map{ - "page": page, - "isNew": true, - "bodyClass": "page-edit-page", - }) -} - -func (ph PagesHandler) Edit(c fiber.Ctx) error { - pageID, err := strconv.ParseInt(c.Params("pageID"), 10, 64) - if err != nil { - return fiber.ErrBadRequest - } - - page, err := ph.PageService.GetPage(c.Context(), pageID) - if err != nil { - return err - } - - return c.Render("pages/edit", fiber.Map{ - "page": page, - "isNew": false, - "bodyClass": "page-edit-page", - }) -} - -func (ph PagesHandler) Create(c fiber.Ctx) error { - var req pages.CreatePageParams - if err := c.Bind().Body(&req); err != nil { - return err - } - - _, err := ph.PageService.CreatePage(c.Context(), req) - if err != nil { - return err - } - - site := models.MustGetSite(c.Context()) - return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID)) -} - -func (ph PagesHandler) Update(c fiber.Ctx) error { - pageID, err := strconv.ParseInt(c.Params("pageID"), 10, 64) - if err != nil { - return fiber.ErrBadRequest - } - - var req pages.CreatePageParams - if err := c.Bind().Body(&req); err != nil { - return err - } - - _, err = ph.PageService.UpdatePage(c.Context(), pageID, req) - if err != nil { - return err - } - - site := models.MustGetSite(c.Context()) - return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID)) -} - -func (ph PagesHandler) Delete(c fiber.Ctx) error { - pageID, err := strconv.ParseInt(c.Params("pageID"), 10, 64) - if err != nil { - return fiber.ErrBadRequest - } - - if err := ph.PageService.DeletePage(c.Context(), pageID); err != nil { - return err - } - - site := models.MustGetSite(c.Context()) - return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID)) -} - -func (ph PagesHandler) Reorder(c fiber.Ctx) error { - var req struct { - PageIDs []int64 `json:"page_ids"` - } - if err := c.Bind().Body(&req); err != nil { - return err - } - - if err := ph.PageService.ReorderPages(c.Context(), req.PageIDs); err != nil { - return err - } - - return c.JSON(fiber.Map{"ok": true}) -} -``` - -- [ ] **Step 2: Register routes in cmds/server.go** - -After the categories route block (~line 150), add: - -```go -pgh := handlers.PagesHandler{PageService: svcs.Pages} -``` - -And routes on `siteGroup`: - -```go -siteGroup.Get("/pages", pgh.Index) -siteGroup.Get("/pages/new", pgh.New) -siteGroup.Get("/pages/:pageID", pgh.Edit) -siteGroup.Post("/pages", pgh.Create) -siteGroup.Post("/pages/reorder", pgh.Reorder) -siteGroup.Post("/pages/:pageID", pgh.Update) -siteGroup.Post("/pages/:pageID/delete", pgh.Delete) -``` - -Add import: `// already imported via handlers package` - -- [ ] **Step 3: Verify it compiles** - -Run: `go build ./...` -Expected: Clean compile - -- [ ] **Step 4: Commit** - -```bash -git add handlers/pages.go cmds/server.go -git commit -m "feat(pages): add pages handler and admin routes" -``` - ---- - -### Task 6: Admin Views - Page List with Drag-and-Drop - -**Files:** -- Create: `views/pages/index.html` -- Create: `assets/js/controllers/pagelist.js` -- Modify: `assets/js/main.js` -- Modify: `views/_common/nav.html` - -- [ ] **Step 1: Add "Pages" to the admin nav bar** - -In `views/_common/nav.html`, add a new `
  • ` after the Categories nav item (after line 14): - -```html -
  • -``` - -- [ ] **Step 2: Write the page list view** - -```html - -
    -
    -
    - New Page -
    -
    - - {{ if .pages }} - - - - - - - - - - - {{ range .pages }} - - - - - - - {{ end }} - -
    TitleSlugNav
    {{ .Title }}{{ .Slug }}{{ if .ShowInNav }}Yes{{ end }}
    - {{ else }} -
    -
    No pages yet.
    -
    - {{ end }} -
    -``` - -- [ ] **Step 3: Write the pagelist Stimulus controller** - -```javascript -// assets/js/controllers/pagelist.js -import { Controller } from "@hotwired/stimulus" -import { showToast } from "../services/toast"; - -export default class PagelistController extends Controller { - static values = { - siteId: Number, - }; - - static targets = ["list"]; - - dragStart(ev) { - this.draggedRow = ev.currentTarget; - ev.currentTarget.classList.add("opacity-50"); - ev.dataTransfer.effectAllowed = "move"; - } - - dragOver(ev) { - ev.preventDefault(); - ev.dataTransfer.dropEffect = "move"; - } - - drop(ev) { - ev.preventDefault(); - const targetRow = ev.currentTarget; - if (this.draggedRow && this.draggedRow !== targetRow) { - const rows = [...this.listTarget.children]; - const draggedIdx = rows.indexOf(this.draggedRow); - const targetIdx = rows.indexOf(targetRow); - if (draggedIdx < targetIdx) { - targetRow.after(this.draggedRow); - } else { - targetRow.before(this.draggedRow); - } - this.saveOrder(); - } - } - - dragEnd(ev) { - ev.currentTarget.classList.remove("opacity-50"); - this.draggedRow = null; - } - - async saveOrder() { - const rows = [...this.listTarget.children]; - const pageIds = rows.map(row => parseInt(row.dataset.pageId, 10)); - - try { - await fetch(`/sites/${this.siteIdValue}/pages/reorder`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - }, - body: JSON.stringify({ page_ids: pageIds }), - }); - } catch (error) { - showToast({ - title: "Error", - body: "Failed to reorder pages.", - }); - } - } -} -``` - -- [ ] **Step 4: Register the controller in main.js** - -Add to `assets/js/main.js`: - -```javascript -import PagelistController from "./controllers/pagelist"; -``` - -And register: - -```javascript -Stimulus.register("pagelist", PagelistController); -``` - -- [ ] **Step 5: Rebuild JS bundle** - -Run: `node esbuild.mjs` -Expected: Clean build, `static/assets/main.js` updated. - -- [ ] **Step 6: Commit** - -```bash -git add views/pages/index.html views/_common/nav.html assets/js/controllers/pagelist.js assets/js/main.js static/assets/main.js -git commit -m "feat(pages): add admin page list with drag-and-drop reorder" -``` - ---- - -### Task 7: Admin Views - Page Edit Form - -**Files:** -- Create: `views/pages/edit.html` - -- [ ] **Step 1: Write the page edit form** - -Two-column layout mirroring the post edit form: title + body on left, slug/page type/show in nav on right sidebar. - -```html - -
    - {{ if .isNew }} -
    - {{ else }} - - {{ end }} - -
    -
    -
    - -
    -
    - -
    -
    - - {{ if not .isNew }} - - {{ end }} -
    -
    -
    -
    -
    Page Settings
    -
    -
    - - -
    Auto-generated from title if left blank.
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    - - {{ if not .isNew }} - - {{ end }} -
    -``` - -- [ ] **Step 2: Verify the app compiles and starts** - -Run: `go build ./...` -Expected: Clean compile. - -- [ ] **Step 3: Commit** - -```bash -git add views/pages/edit.html -git commit -m "feat(pages): add admin page edit form with sidebar" -``` - ---- - -### Task 8: Publisher and pubmodel Changes - -**Files:** -- Modify: `models/pubmodel/sites.go` -- Modify: `services/publisher/service.go` - -- [ ] **Step 1: Add Pages field to pubmodel.Site** - -In `models/pubmodel/sites.go`, add to the `Site` struct: - -```go -Pages []*models.Page -``` - -- [ ] **Step 2: Populate pages in the publisher** - -In `services/publisher/service.go`, in the `Publish` method, after fetching categories (~line 66), add: - -```go -// Fetch pages -sitePages, err := p.db.SelectPagesOfSite(ctx, site.ID) -if err != nil { - return err -} -``` - -Then in the `pubSite` construction (~line 73), add the `Pages` field: - -```go -Pages: sitePages, -``` - -- [ ] **Step 3: Verify it compiles** - -Run: `go build ./...` -Expected: Clean compile. - -- [ ] **Step 4: Commit** - -```bash -git add models/pubmodel/sites.go services/publisher/service.go -git commit -m "feat(pages): populate pages in publisher for site generation" -``` - ---- - -### Task 9: Site Builder - Render Pages - -**Files:** -- Create: `providers/sitebuilder/render_pages.go` -- Modify: `providers/sitebuilder/tmpls.go` -- Modify: `providers/sitebuilder/builder.go` -- Create: `layouts/simplecss/templates/pages_single.html` -- Modify: `providers/sitebuilder/builder_test.go` - -- [ ] **Step 1: Add template types and constant** - -In `providers/sitebuilder/tmpls.go`, add the template name constant: - -```go -// tmplNamePageSingle is the template for a single page (pageSingleData) -tmplNamePageSingle = "pages_single.html" -``` - -And the data struct: - -```go -type pageSingleData struct { - commonData - Page *models.Page - HTML template.HTML -} -``` - -- [ ] **Step 2: Create the renderPages method** - -```go -// providers/sitebuilder/render_pages.go -package sitebuilder - -import ( - "bytes" - "context" - "html/template" - "io" - - "lmika.dev/lmika/weiro/models" -) - -func (b *Builder) renderPages(bctx buildContext) error { - for _, page := range b.site.Pages { - - var md bytes.Buffer - if err := b.mdRenderer.RenderTo(context.Background(), &md, page.Body); err != nil { - return err - } - - data := pageSingleData{ - commonData: commonData{Site: b.site}, - Page: page, - HTML: template.HTML(md.String()), - } - - path := "/" + page.Slug - if err := b.createAtPath(bctx, path, func(f io.Writer) error { - return b.renderTemplate(f, tmplNamePageSingle, data) - }); err != nil { - return err - } - } - return nil -} -``` - -- [ ] **Step 3: Call renderPages after eg.Wait() in BuildSite** - -In `providers/sitebuilder/builder.go`, modify the `BuildSite` method. Replace: - -```go -return eg.Wait() -``` - -With: - -```go -if err := eg.Wait(); err != nil { - return err -} - -// Render pages last so they can override auto-generated content -return b.renderPages(buildCtx) -``` - -- [ ] **Step 4: Create the generated site template** - -```html - -{{ if .Page.Title }}

    {{ .Page.Title }}

    {{ end }} -{{ .HTML }} -``` - -- [ ] **Step 5: Add pages to the builder test** - -In `providers/sitebuilder/builder_test.go`, add `"pages_single.html"` to the `tmpls` MapFS: - -```go -"pages_single.html": {Data: []byte(`{{ if .Page.Title }}

    {{ .Page.Title }}

    {{ end }}{{ .HTML }}`)}, -``` - -Add pages to the `site` struct: - -```go -Pages: []*models.Page{ - {Title: "About", Slug: "about", Body: "About this site"}, -}, -``` - -Add to `wantFiles`: - -```go -"about/index.html": "

    About

    About this site

    \n", -``` - -- [ ] **Step 6: Run the builder test** - -Run: `go test ./providers/sitebuilder/ -v` -Expected: PASS - -- [ ] **Step 7: Commit** - -```bash -git add providers/sitebuilder/render_pages.go providers/sitebuilder/tmpls.go providers/sitebuilder/builder.go layouts/simplecss/templates/pages_single.html providers/sitebuilder/builder_test.go -git commit -m "feat(pages): render pages in site builder after all other content" -``` - ---- - -### Task 10: Integration Test - Full Compile and Verify - -**Files:** None (verification only) - -- [ ] **Step 1: Run all tests** - -Run: `go test ./...` -Expected: All tests pass. - -- [ ] **Step 2: Verify clean build** - -Run: `go build ./...` -Expected: Clean compile, no errors. - -- [ ] **Step 3: Commit any fixes if needed** - -Only if previous steps required adjustments. diff --git a/docs/superpowers/specs/2026-03-22-pages-design.md b/docs/superpowers/specs/2026-03-22-pages-design.md deleted file mode 100644 index cc17417..0000000 --- a/docs/superpowers/specs/2026-03-22-pages-design.md +++ /dev/null @@ -1,148 +0,0 @@ -# Arbitrary Pages Feature Design - -## Overview - -Allow users to create arbitrary pages for their site. Each page has a title, user-editable slug, markdown body, page type, nav visibility flag, and sort order. Pages are a separate entity from posts with their own admin section and generated site template. Pages rendered at conflicting slugs silently override auto-generated content. - -## Data Layer - -### New `pages` table - -```sql -CREATE TABLE pages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - site_id INTEGER NOT NULL, - guid TEXT NOT NULL, - title TEXT NOT NULL, - slug TEXT NOT NULL, - body TEXT NOT NULL, - page_type INTEGER NOT NULL DEFAULT 0, - show_in_nav INTEGER NOT NULL DEFAULT 0, - sort_order INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE -); -CREATE INDEX idx_pages_site ON pages (site_id); -CREATE UNIQUE INDEX idx_pages_guid ON pages (guid); -CREATE UNIQUE INDEX idx_pages_site_slug ON pages (site_id, slug); -``` - -### Model - -```go -type Page struct { - ID int64 - SiteID int64 - GUID string - Title string - Slug string - Body string - PageType int - ShowInNav bool - SortOrder int - CreatedAt time.Time - UpdatedAt time.Time -} -``` - -Page type constants: `PageTypeNormal = 0` (extensible later for archive, search, etc.). - -### SQL queries - -- `SelectPagesOfSite(siteID)` — all pages for a site, ordered by `sort_order ASC` -- `SelectPage(id)` — single page by ID -- `SelectPageByGUID(guid)` — single page by GUID -- `InsertPage` — create new page, returns ID -- `UpdatePage` — update page fields -- `DeletePage(id)` — delete page -- `UpdatePageSortOrder(id, sortOrder)` — update sort order for a single page - -## Admin Section - -### Navigation - -Add "Pages" item to the admin nav bar (`views/_common/nav.html`), linking to `/sites/:siteID/pages`. - -### Routes - -``` -GET /sites/:siteID/pages - List pages -GET /sites/:siteID/pages/new - New page form -GET /sites/:siteID/pages/:pageID - Edit page form -POST /sites/:siteID/pages - Create/update page -DELETE /sites/:siteID/pages/:pageID - Delete page -POST /sites/:siteID/pages/reorder - Update sort order (AJAX) -``` - -### Page list view (`views/pages/index.html`) - -- Lists pages ordered by `sort_order` -- Each row shows title, slug, and nav visibility indicator -- Drag-and-drop reordering via Stimulus + HTML drag API -- On drop, sends new order to `POST /pages/reorder` via AJAX -- "New Page" button - -### Page edit form (`views/pages/edit.html`) - -Two-column layout mirroring the post edit form: - -**Main area (left):** -- Title input -- Body textarea (markdown) - -**Sidebar (right):** -- Slug (editable text input, auto-derived from title via client-side JS, user can override) -- Page Type (select dropdown, just "Normal" for now) -- Show in Nav (checkbox) - -Save button below. - -### Service layer (`services/pages/`) - -- `Service` struct with DB provider dependency -- `CreatePage(ctx, params)` — generates GUID, derives slug from title if not provided, sets timestamps -- `UpdatePage(ctx, params)` — updates fields, sets `updated_at` -- `DeletePage(ctx, pageID)` — deletes page -- `ListPages(ctx)` — returns all pages for the site from context, ordered by `sort_order` -- `GetPage(ctx, pageID)` — returns single page -- `ReorderPages(ctx, pageIDs []int64)` — accepts ordered list of page IDs, updates `sort_order` for each (sort_order = index in list) - -### Handler (`handlers/pages.go`) - -- `PagesHandler` struct with `PageService` -- Standard CRUD handlers following the existing posts handler pattern -- `Reorder` handler accepts JSON array of page IDs, calls `ReorderPages` - -## Generated Site - -### Template - -New template `pages_single.html` — receives rendered page HTML, rendered inside `layout_main.html` (same wrapping as posts). - -Template data: -```go -type pageSingleData struct { - commonData - Page *models.Page - HTML template.HTML -} -``` - -### Builder changes - -New method `renderPages` on the builder: -- Iterates all pages from `pubmodel.Site.Pages` -- For each page, renders markdown body and writes to the page's slug path using `createAtPath` -- Pages are rendered **after** all other content (posts, post lists, categories, feeds, uploads, static assets) -- This ensures pages at conflicting slugs silently overwrite auto-generated content -- Implementation: `renderPages` runs as a sequential step after `eg.Wait()` returns in `BuildSite` - -### Publisher changes - -- `pubmodel.Site` gets a new `Pages []models.Page` field -- The publisher fetches all pages for the site via `SelectPagesOfSite` and populates this field - -## Approach - -Pages are a separate entity from posts with their own table, service, handler, and templates. The override mechanism is file-system-based: the site builder renders pages last, so any page slug that conflicts with an auto-generated path wins by overwriting the file. The `show_in_nav` field is stored and editable in admin but not yet consumed by the generated site layout — that integration is deferred for a future change. diff --git a/handlers/pages.go b/handlers/pages.go deleted file mode 100644 index abefb41..0000000 --- a/handlers/pages.go +++ /dev/null @@ -1,118 +0,0 @@ -package handlers - -import ( - "fmt" - "strconv" - - "github.com/gofiber/fiber/v3" - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/services/pages" -) - -type PagesHandler struct { - PageService *pages.Service -} - -func (ph PagesHandler) Index(c fiber.Ctx) error { - pagesList, err := ph.PageService.ListPages(c.Context()) - if err != nil { - return err - } - - return c.Render("pages/index", fiber.Map{ - "pages": pagesList, - }) -} - -func (ph PagesHandler) New(c fiber.Ctx) error { - page := models.Page{ - GUID: models.NewNanoID(), - } - return c.Render("pages/edit", fiber.Map{ - "page": page, - "isNew": true, - "bodyClass": "post-edit-page", - }) -} - -func (ph PagesHandler) Edit(c fiber.Ctx) error { - pageID, err := strconv.ParseInt(c.Params("pageID"), 10, 64) - if err != nil { - return fiber.ErrBadRequest - } - - page, err := ph.PageService.GetPage(c.Context(), pageID) - if err != nil { - return err - } - - return c.Render("pages/edit", fiber.Map{ - "page": page, - "isNew": false, - "bodyClass": "post-edit-page", - }) -} - -func (ph PagesHandler) Create(c fiber.Ctx) error { - var req pages.CreatePageParams - if err := c.Bind().Body(&req); err != nil { - return err - } - - _, err := ph.PageService.CreatePage(c.Context(), req) - if err != nil { - return err - } - - site := models.MustGetSite(c.Context()) - return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID)) -} - -func (ph PagesHandler) Update(c fiber.Ctx) error { - pageID, err := strconv.ParseInt(c.Params("pageID"), 10, 64) - if err != nil { - return fiber.ErrBadRequest - } - - var req pages.CreatePageParams - if err := c.Bind().Body(&req); err != nil { - return err - } - - _, err = ph.PageService.UpdatePage(c.Context(), pageID, req) - if err != nil { - return err - } - - site := models.MustGetSite(c.Context()) - return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID)) -} - -func (ph PagesHandler) Delete(c fiber.Ctx) error { - pageID, err := strconv.ParseInt(c.Params("pageID"), 10, 64) - if err != nil { - return fiber.ErrBadRequest - } - - if err := ph.PageService.DeletePage(c.Context(), pageID); err != nil { - return err - } - - site := models.MustGetSite(c.Context()) - return c.Redirect().To(fmt.Sprintf("/sites/%v/pages", site.ID)) -} - -func (ph PagesHandler) Reorder(c fiber.Ctx) error { - var req struct { - PageIDs []int64 `json:"page_ids"` - } - if err := c.Bind().Body(&req); err != nil { - return err - } - - if err := ph.PageService.ReorderPages(c.Context(), req.PageIDs); err != nil { - return err - } - - return c.JSON(fiber.Map{"ok": true}) -} diff --git a/layouts/simplecss/templates/categories_single.html b/layouts/simplecss/templates/categories_single.html index 133ad8d..e9e7116 100644 --- a/layouts/simplecss/templates/categories_single.html +++ b/layouts/simplecss/templates/categories_single.html @@ -11,7 +11,7 @@ {{ end }} {{ if or .PrevURL .NextURL }} {{ end }} diff --git a/layouts/simplecss/templates/pages_single.html b/layouts/simplecss/templates/pages_single.html deleted file mode 100644 index 6883c3e..0000000 --- a/layouts/simplecss/templates/pages_single.html +++ /dev/null @@ -1,2 +0,0 @@ -{{ if .Page.Title }}

    {{ .Page.Title }}

    {{ end }} -{{ .HTML }} diff --git a/layouts/simplecss/templates/posts_list.html b/layouts/simplecss/templates/posts_list.html index 6a71533..6a2eca6 100644 --- a/layouts/simplecss/templates/posts_list.html +++ b/layouts/simplecss/templates/posts_list.html @@ -8,7 +8,7 @@ {{ end }} {{ if or .PrevURL .NextURL }} {{ end }} diff --git a/models/errors.go b/models/errors.go index 3efadbc..eda780c 100644 --- a/models/errors.go +++ b/models/errors.go @@ -7,4 +7,4 @@ var PermissionError = errors.New("permission denied") var NotFoundError = errors.New("not found") var SiteRequiredError = errors.New("site required") var DeleteDebounceError = errors.New("permanent delete too soon, try again in a few seconds") -var SlugConflictError = errors.New("a record with this slug already exists") +var SlugConflictError = errors.New("a category with this slug already exists") diff --git a/models/pages.go b/models/pages.go deleted file mode 100644 index 1022120..0000000 --- a/models/pages.go +++ /dev/null @@ -1,45 +0,0 @@ -package models - -import ( - "strings" - "time" - "unicode" -) - -const ( - PageTypeNormal = 0 -) - -type Page struct { - ID int64 `json:"id"` - SiteID int64 `json:"site_id"` - GUID string `json:"guid"` - Title string `json:"title"` - Slug string `json:"slug"` - Body string `json:"body"` - PageType int `json:"page_type"` - ShowInNav bool `json:"show_in_nav"` - SortOrder int `json:"sort_order"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// GeneratePageSlug creates a URL-safe slug from a page title. -// e.g. "About Me" -> "about-me" -func GeneratePageSlug(title string) string { - var sb strings.Builder - prevDash := false - for _, c := range strings.TrimSpace(title) { - if unicode.IsLetter(c) || unicode.IsNumber(c) { - sb.WriteRune(unicode.ToLower(c)) - prevDash = false - } else if unicode.IsSpace(c) || c == '-' || c == '_' { - if !prevDash && sb.Len() > 0 { - sb.WriteRune('-') - prevDash = true - } - } - } - result := sb.String() - return strings.TrimRight(result, "-") -} diff --git a/models/pages_test.go b/models/pages_test.go deleted file mode 100644 index 831b31f..0000000 --- a/models/pages_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package models_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "lmika.dev/lmika/weiro/models" -) - -func TestGeneratePageSlug(t *testing.T) { - tests := []struct { - title string - want string - }{ - {"About Me", "about-me"}, - {" Contact Us ", "contact-us"}, - {"Hello---World", "hello-world"}, - {"FAQ", "faq"}, - {"", ""}, - } - for _, tt := range tests { - t.Run(tt.title, func(t *testing.T) { - assert.Equal(t, tt.want, models.GeneratePageSlug(tt.title)) - }) - } -} diff --git a/models/pubmodel/sites.go b/models/pubmodel/sites.go index 38ba614..a8862c4 100644 --- a/models/pubmodel/sites.go +++ b/models/pubmodel/sites.go @@ -18,5 +18,4 @@ type Site struct { Categories []models.CategoryWithCount PostIterByCategory func(ctx context.Context, categoryID int64) iter.Seq[models.Maybe[*models.Post]] CategoriesOfPost func(ctx context.Context, postID int64) ([]*models.Category, error) - Pages []*models.Page } diff --git a/providers/db/gen/sqlgen/models.go b/providers/db/gen/sqlgen/models.go index 3df1193..ae58594 100644 --- a/providers/db/gen/sqlgen/models.go +++ b/providers/db/gen/sqlgen/models.go @@ -15,20 +15,6 @@ type Category struct { UpdatedAt int64 } -type Page struct { - ID int64 - SiteID int64 - Guid string - Title string - Slug string - Body string - PageType int64 - ShowInNav int64 - SortOrder int64 - CreatedAt int64 - UpdatedAt int64 -} - type PendingUpload struct { ID int64 SiteID int64 diff --git a/providers/db/gen/sqlgen/pages.sql.go b/providers/db/gen/sqlgen/pages.sql.go deleted file mode 100644 index 1d53291..0000000 --- a/providers/db/gen/sqlgen/pages.sql.go +++ /dev/null @@ -1,219 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.28.0 -// source: pages.sql - -package sqlgen - -import ( - "context" -) - -const deletePage = `-- name: DeletePage :exec -DELETE FROM pages WHERE id = ? -` - -func (q *Queries) DeletePage(ctx context.Context, id int64) error { - _, err := q.db.ExecContext(ctx, deletePage, id) - return err -} - -const insertPage = `-- name: InsertPage :one -INSERT INTO pages ( - site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id -` - -type InsertPageParams struct { - SiteID int64 - Guid string - Title string - Slug string - Body string - PageType int64 - ShowInNav int64 - SortOrder int64 - CreatedAt int64 - UpdatedAt int64 -} - -func (q *Queries) InsertPage(ctx context.Context, arg InsertPageParams) (int64, error) { - row := q.db.QueryRowContext(ctx, insertPage, - arg.SiteID, - arg.Guid, - arg.Title, - arg.Slug, - arg.Body, - arg.PageType, - arg.ShowInNav, - arg.SortOrder, - arg.CreatedAt, - arg.UpdatedAt, - ) - var id int64 - err := row.Scan(&id) - return id, err -} - -const selectPage = `-- name: SelectPage :one -SELECT id, site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at FROM pages WHERE id = ? LIMIT 1 -` - -func (q *Queries) SelectPage(ctx context.Context, id int64) (Page, error) { - row := q.db.QueryRowContext(ctx, selectPage, id) - var i Page - err := row.Scan( - &i.ID, - &i.SiteID, - &i.Guid, - &i.Title, - &i.Slug, - &i.Body, - &i.PageType, - &i.ShowInNav, - &i.SortOrder, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const selectPageByGUID = `-- name: SelectPageByGUID :one -SELECT id, site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at FROM pages WHERE guid = ? LIMIT 1 -` - -func (q *Queries) SelectPageByGUID(ctx context.Context, guid string) (Page, error) { - row := q.db.QueryRowContext(ctx, selectPageByGUID, guid) - var i Page - err := row.Scan( - &i.ID, - &i.SiteID, - &i.Guid, - &i.Title, - &i.Slug, - &i.Body, - &i.PageType, - &i.ShowInNav, - &i.SortOrder, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const selectPageBySlugAndSite = `-- name: SelectPageBySlugAndSite :one -SELECT id, site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at FROM pages WHERE site_id = ? AND slug = ? LIMIT 1 -` - -type SelectPageBySlugAndSiteParams struct { - SiteID int64 - Slug string -} - -func (q *Queries) SelectPageBySlugAndSite(ctx context.Context, arg SelectPageBySlugAndSiteParams) (Page, error) { - row := q.db.QueryRowContext(ctx, selectPageBySlugAndSite, arg.SiteID, arg.Slug) - var i Page - err := row.Scan( - &i.ID, - &i.SiteID, - &i.Guid, - &i.Title, - &i.Slug, - &i.Body, - &i.PageType, - &i.ShowInNav, - &i.SortOrder, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const selectPagesOfSite = `-- name: SelectPagesOfSite :many -SELECT id, site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at FROM pages -WHERE site_id = ? ORDER BY sort_order ASC -` - -func (q *Queries) SelectPagesOfSite(ctx context.Context, siteID int64) ([]Page, error) { - rows, err := q.db.QueryContext(ctx, selectPagesOfSite, siteID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Page - for rows.Next() { - var i Page - if err := rows.Scan( - &i.ID, - &i.SiteID, - &i.Guid, - &i.Title, - &i.Slug, - &i.Body, - &i.PageType, - &i.ShowInNav, - &i.SortOrder, - &i.CreatedAt, - &i.UpdatedAt, - ); 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 updatePage = `-- name: UpdatePage :exec -UPDATE pages SET - title = ?, - slug = ?, - body = ?, - page_type = ?, - show_in_nav = ?, - updated_at = ? -WHERE id = ? -` - -type UpdatePageParams struct { - Title string - Slug string - Body string - PageType int64 - ShowInNav int64 - UpdatedAt int64 - ID int64 -} - -func (q *Queries) UpdatePage(ctx context.Context, arg UpdatePageParams) error { - _, err := q.db.ExecContext(ctx, updatePage, - arg.Title, - arg.Slug, - arg.Body, - arg.PageType, - arg.ShowInNav, - arg.UpdatedAt, - arg.ID, - ) - return err -} - -const updatePageSortOrder = `-- name: UpdatePageSortOrder :exec -UPDATE pages SET sort_order = ? WHERE id = ? -` - -type UpdatePageSortOrderParams struct { - SortOrder int64 - ID int64 -} - -func (q *Queries) UpdatePageSortOrder(ctx context.Context, arg UpdatePageSortOrderParams) error { - _, err := q.db.ExecContext(ctx, updatePageSortOrder, arg.SortOrder, arg.ID) - return err -} diff --git a/providers/db/pages.go b/providers/db/pages.go deleted file mode 100644 index 1e5b9fc..0000000 --- a/providers/db/pages.go +++ /dev/null @@ -1,115 +0,0 @@ -package db - -import ( - "context" - "time" - - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/providers/db/gen/sqlgen" -) - -func (db *Provider) SelectPagesOfSite(ctx context.Context, siteID int64) ([]*models.Page, error) { - rows, err := db.queries.SelectPagesOfSite(ctx, siteID) - if err != nil { - return nil, err - } - pages := make([]*models.Page, len(rows)) - for i, row := range rows { - pages[i] = dbPageToPage(row) - } - return pages, nil -} - -func (db *Provider) SelectPage(ctx context.Context, id int64) (*models.Page, error) { - row, err := db.queries.SelectPage(ctx, id) - if err != nil { - return nil, err - } - return dbPageToPage(row), nil -} - -func (db *Provider) SelectPageByGUID(ctx context.Context, guid string) (*models.Page, error) { - row, err := db.queries.SelectPageByGUID(ctx, guid) - if err != nil { - return nil, err - } - return dbPageToPage(row), nil -} - -func (db *Provider) SelectPageBySlugAndSite(ctx context.Context, siteID int64, slug string) (*models.Page, error) { - row, err := db.queries.SelectPageBySlugAndSite(ctx, sqlgen.SelectPageBySlugAndSiteParams{ - SiteID: siteID, - Slug: slug, - }) - if err != nil { - return nil, err - } - return dbPageToPage(row), nil -} - -func (db *Provider) SavePage(ctx context.Context, page *models.Page) error { - if page.ID == 0 { - showInNav := int64(0) - if page.ShowInNav { - showInNav = 1 - } - newID, err := db.queries.InsertPage(ctx, sqlgen.InsertPageParams{ - SiteID: page.SiteID, - Guid: page.GUID, - Title: page.Title, - Slug: page.Slug, - Body: page.Body, - PageType: int64(page.PageType), - ShowInNav: showInNav, - SortOrder: int64(page.SortOrder), - CreatedAt: timeToInt(page.CreatedAt), - UpdatedAt: timeToInt(page.UpdatedAt), - }) - if err != nil { - return err - } - page.ID = newID - return nil - } - - showInNav := int64(0) - if page.ShowInNav { - showInNav = 1 - } - return db.queries.UpdatePage(ctx, sqlgen.UpdatePageParams{ - Title: page.Title, - Slug: page.Slug, - Body: page.Body, - PageType: int64(page.PageType), - ShowInNav: showInNav, - UpdatedAt: timeToInt(page.UpdatedAt), - ID: page.ID, - }) -} - -func (db *Provider) UpdatePageSortOrder(ctx context.Context, id int64, sortOrder int) error { - return db.queries.UpdatePageSortOrder(ctx, sqlgen.UpdatePageSortOrderParams{ - SortOrder: int64(sortOrder), - ID: id, - }) -} - -func (db *Provider) DeletePage(ctx context.Context, id int64) error { - return db.queries.DeletePage(ctx, id) -} - -func dbPageToPage(row sqlgen.Page) *models.Page { - return &models.Page{ - ID: row.ID, - SiteID: row.SiteID, - GUID: row.Guid, - Title: row.Title, - Slug: row.Slug, - Body: row.Body, - PageType: int(row.PageType), - ShowInNav: row.ShowInNav != 0, - SortOrder: int(row.SortOrder), - CreatedAt: time.Unix(row.CreatedAt, 0).UTC(), - UpdatedAt: time.Unix(row.UpdatedAt, 0).UTC(), - } -} diff --git a/providers/sitebuilder/builder.go b/providers/sitebuilder/builder.go index 71ce926..9e5199d 100644 --- a/providers/sitebuilder/builder.go +++ b/providers/sitebuilder/builder.go @@ -118,12 +118,7 @@ func (b *Builder) BuildSite(outDir string) error { // Build static assets eg.Go(func() error { return b.writeStaticAssets(buildCtx) }) - if err := eg.Wait(); err != nil { - return err - } - - // Render pages last so they can override auto-generated content - return b.renderPages(buildCtx) + return eg.Wait() } func (b *Builder) renderPostListWithCategories(bctx buildContext, ctx context.Context) error { @@ -166,10 +161,14 @@ func (b *Builder) renderPostListWithCategories(bctx buildContext, ctx context.Co var prevURL, nextURL string if page > 1 { - prevURL = fmt.Sprintf("%v/%d", b.opts.BasePostList, page-1) + if page == 2 { + prevURL = "/posts/" + } else { + prevURL = fmt.Sprintf("/posts/%d/", page-1) + } } if page < totalPages { - nextURL = fmt.Sprintf("%v/%d", b.opts.BasePostList, page+1) + nextURL = fmt.Sprintf("/posts/%d/", page+1) } pl := postListData{ @@ -183,9 +182,9 @@ func (b *Builder) renderPostListWithCategories(bctx buildContext, ctx context.Co // Page 1 renders at both root and /posts/ var paths []string if page == 1 { - paths = []string{"", fmt.Sprintf("%v/1", b.opts.BasePostList)} + paths = []string{"", "/posts"} } else { - paths = []string{fmt.Sprintf("%v/%d", b.opts.BasePostList, page)} + paths = []string{fmt.Sprintf("/posts/%d", page)} } for _, path := range paths { diff --git a/providers/sitebuilder/builder_test.go b/providers/sitebuilder/builder_test.go index 3fec74f..a5a9bbf 100644 --- a/providers/sitebuilder/builder_test.go +++ b/providers/sitebuilder/builder_test.go @@ -22,7 +22,6 @@ func TestBuilder_BuildSite(t *testing.T) { "layout_main.html": {Data: []byte(`{{ .Body }}`)}, "categories_list.html": {Data: []byte(`{{ range .Categories}}{{.Name}},{{ end }}`)}, "categories_single.html": {Data: []byte(`

    {{.Category.Name}}

    `)}, - "pages_single.html": {Data: []byte(`{{ if .Page.Title }}

    {{ .Page.Title }}

    {{ end }}{{ .HTML }}`)}, } posts := []*models.Post{ @@ -50,15 +49,11 @@ func TestBuilder_BuildSite(t *testing.T) { } } }, - Pages: []*models.Page{ - {Title: "About", Slug: "about", Body: "About this site"}, - }, } wantFiles := map[string]string{ "2026/02/18/test-post/index.html": "

    This is a test post

    \n", "2026/02/20/another-post/index.html": "

    This is another test post

    \n", "index.html": "Test Post,Another Post,", - "about/index.html": "

    About

    About this site

    \n", } outDir := t.TempDir() diff --git a/providers/sitebuilder/render_pages.go b/providers/sitebuilder/render_pages.go deleted file mode 100644 index 6183088..0000000 --- a/providers/sitebuilder/render_pages.go +++ /dev/null @@ -1,31 +0,0 @@ -package sitebuilder - -import ( - "bytes" - "context" - "html/template" - "io" -) - -func (b *Builder) renderPages(bctx buildContext) error { - for _, page := range b.site.Pages { - var md bytes.Buffer - if err := b.mdRenderer.RenderTo(context.Background(), &md, page.Body); err != nil { - return err - } - - data := pageSingleData{ - commonData: commonData{Site: b.site}, - Page: page, - HTML: template.HTML(md.String()), - } - - path := "/" + page.Slug - if err := b.createAtPath(bctx, path, func(f io.Writer) error { - return b.renderTemplate(f, tmplNamePageSingle, data) - }); err != nil { - return err - } - } - return nil -} diff --git a/providers/sitebuilder/tmpls.go b/providers/sitebuilder/tmpls.go index 029cab0..e0ece37 100644 --- a/providers/sitebuilder/tmpls.go +++ b/providers/sitebuilder/tmpls.go @@ -26,16 +26,12 @@ const ( // tmplNameCategorySingle is the template for a single category page tmplNameCategorySingle = "categories_single.html" - - // tmplNamePageSingle is the template for a single page (pageSingleData) - tmplNamePageSingle = "pages_single.html" ) type Options struct { - BasePosts string // BasePosts is the base path for posts. - BasePostList string // BasePostList is the base path for post lists. - BaseUploads string // BaseUploads is the base path for uploads. - BaseStatic string // BaseStatic is the base path for static assets. + BasePosts string // BasePosts is the base path for posts. + BaseUploads string // BaseUploads is the base path for uploads. + BaseStatic string // BaseStatic is the base path for static assets. // TemplatesFS provides the raw templates for rendering the site. TemplatesFS fs.FS @@ -96,9 +92,3 @@ type categorySingleData struct { PrevURL string NextURL string } - -type pageSingleData struct { - commonData - Page *models.Page - HTML template.HTML -} diff --git a/services/pages/service.go b/services/pages/service.go deleted file mode 100644 index 8a82bc0..0000000 --- a/services/pages/service.go +++ /dev/null @@ -1,198 +0,0 @@ -package pages - -import ( - "context" - "strings" - "time" - - "lmika.dev/lmika/weiro/models" - "lmika.dev/lmika/weiro/providers/db" - "lmika.dev/lmika/weiro/services/publisher" -) - -type CreatePageParams struct { - GUID string `form:"guid" json:"guid"` - Title string `form:"title" json:"title"` - Slug string `form:"slug" json:"slug"` - Body string `form:"body" json:"body"` - PageType int `form:"page_type" json:"page_type"` - ShowInNav bool `form:"show_in_nav" json:"show_in_nav"` -} - -type Service struct { - db *db.Provider - publisher *publisher.Queue -} - -func New(db *db.Provider, publisher *publisher.Queue) *Service { - return &Service{db: db, publisher: publisher} -} - -func (s *Service) ListPages(ctx context.Context) ([]*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - return s.db.SelectPagesOfSite(ctx, site.ID) -} - -func (s *Service) GetPage(ctx context.Context, id int64) (*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return nil, err - } - if page.SiteID != site.ID { - return nil, models.NotFoundError - } - return page, nil -} - -func (s *Service) CreatePage(ctx context.Context, params CreatePageParams) (*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - - now := time.Now() - slug := params.Slug - if slug == "" { - slug = models.GeneratePageSlug(params.Title) - } - - if !strings.HasPrefix(slug, "/") { - slug = "/" + slug - } - - // Check slug collision - if _, err := s.db.SelectPageBySlugAndSite(ctx, site.ID, slug); err == nil { - return nil, models.SlugConflictError - } else if !db.ErrorIsNoRows(err) { - return nil, err - } - - // Determine sort order: place at end - existingPages, err := s.db.SelectPagesOfSite(ctx, site.ID) - if err != nil { - return nil, err - } - sortOrder := len(existingPages) - - page := &models.Page{ - SiteID: site.ID, - GUID: params.GUID, - Title: params.Title, - Slug: slug, - Body: params.Body, - PageType: params.PageType, - ShowInNav: params.ShowInNav, - SortOrder: sortOrder, - CreatedAt: now, - UpdatedAt: now, - } - if page.GUID == "" { - page.GUID = models.NewNanoID() - } - - if err := s.db.SavePage(ctx, page); err != nil { - return nil, err - } - - s.publisher.Queue(site) - return page, nil -} - -func (s *Service) UpdatePage(ctx context.Context, id int64, params CreatePageParams) (*models.Page, error) { - site, ok := models.GetSite(ctx) - if !ok { - return nil, models.SiteRequiredError - } - - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return nil, err - } - if page.SiteID != site.ID { - return nil, models.NotFoundError - } - - slug := params.Slug - if slug == "" { - slug = models.GeneratePageSlug(params.Title) - } - - if !strings.HasPrefix(slug, "/") { - slug = "/" + slug - } - - // Check slug collision (exclude self) - if existing, err := s.db.SelectPageBySlugAndSite(ctx, site.ID, slug); err == nil && existing.ID != page.ID { - return nil, models.SlugConflictError - } else if err != nil && !db.ErrorIsNoRows(err) { - return nil, err - } - - page.Title = params.Title - page.Slug = slug - page.Body = params.Body - page.PageType = params.PageType - page.ShowInNav = params.ShowInNav - page.UpdatedAt = time.Now() - - if err := s.db.SavePage(ctx, page); err != nil { - return nil, err - } - - s.publisher.Queue(site) - return page, nil -} - -func (s *Service) DeletePage(ctx context.Context, id int64) error { - site, ok := models.GetSite(ctx) - if !ok { - return models.SiteRequiredError - } - - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return err - } - if page.SiteID != site.ID { - return models.NotFoundError - } - - if err := s.db.DeletePage(ctx, id); err != nil { - return err - } - - s.publisher.Queue(site) - return nil -} - -func (s *Service) ReorderPages(ctx context.Context, pageIDs []int64) error { - site, ok := models.GetSite(ctx) - if !ok { - return models.SiteRequiredError - } - - // Verify all pages belong to this site - for i, id := range pageIDs { - page, err := s.db.SelectPage(ctx, id) - if err != nil { - return err - } - if page.SiteID != site.ID { - return models.NotFoundError - } - if err := s.db.UpdatePageSortOrder(ctx, id, i); err != nil { - return err - } - } - - s.publisher.Queue(site) - return nil -} diff --git a/services/publisher/service.go b/services/publisher/service.go index adfcdd7..939817a 100644 --- a/services/publisher/service.go +++ b/services/publisher/service.go @@ -65,12 +65,6 @@ func (p *Publisher) Publish(ctx context.Context, site models.Site) error { }) } - // Fetch pages - sitePages, err := p.db.SelectPagesOfSite(ctx, site.ID) - if err != nil { - return err - } - for _, target := range targets { if !target.Enabled { continue @@ -90,7 +84,6 @@ func (p *Publisher) Publish(ctx context.Context, site models.Site) error { CategoriesOfPost: func(ctx context.Context, postID int64) ([]*models.Category, error) { return p.db.SelectCategoriesOfPost(ctx, postID) }, - Pages: sitePages, OpenUpload: func(u models.Upload) (io.ReadCloser, error) { return p.up.OpenUpload(site, u) }, @@ -121,14 +114,13 @@ func (p *Publisher) publishSite(ctx context.Context, pubSite pubmodel.Site, targ } sb, err := sitebuilder.New(pubSite, sitebuilder.Options{ - BasePosts: "/posts", - BasePostList: "/pages", - BaseUploads: "/uploads", - BaseStatic: "/static", - TemplatesFS: templateFS, - StaticFS: staticFS, - FeedItems: 30, - RenderTZ: renderTZ, + BasePosts: "/posts", + BaseUploads: "/uploads", + BaseStatic: "/static", + TemplatesFS: templateFS, + StaticFS: staticFS, + FeedItems: 30, + RenderTZ: renderTZ, }) if err != nil { return err diff --git a/services/services.go b/services/services.go index 852dea3..beb6727 100644 --- a/services/services.go +++ b/services/services.go @@ -8,7 +8,6 @@ import ( "lmika.dev/lmika/weiro/providers/uploadfiles" "lmika.dev/lmika/weiro/services/auth" "lmika.dev/lmika/weiro/services/categories" - "lmika.dev/lmika/weiro/services/pages" "lmika.dev/lmika/weiro/services/posts" "lmika.dev/lmika/weiro/services/publisher" "lmika.dev/lmika/weiro/services/sites" @@ -24,7 +23,6 @@ type Services struct { Sites *sites.Service Uploads *uploads.Service Categories *categories.Service - Pages *pages.Service } func New(cfg config.Config) (*Services, error) { @@ -42,7 +40,6 @@ func New(cfg config.Config) (*Services, error) { siteService := sites.New(dbp) uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending")) categoriesService := categories.New(dbp, publisherQueue) - pagesService := pages.New(dbp, publisherQueue) return &Services{ DB: dbp, @@ -53,7 +50,6 @@ func New(cfg config.Config) (*Services, error) { Sites: siteService, Uploads: uploadService, Categories: categoriesService, - Pages: pagesService, }, nil } diff --git a/sql/queries/pages.sql b/sql/queries/pages.sql deleted file mode 100644 index 0df22ff..0000000 --- a/sql/queries/pages.sql +++ /dev/null @@ -1,34 +0,0 @@ --- name: SelectPagesOfSite :many -SELECT * FROM pages -WHERE site_id = ? ORDER BY sort_order ASC; - --- name: SelectPage :one -SELECT * FROM pages WHERE id = ? LIMIT 1; - --- name: SelectPageByGUID :one -SELECT * FROM pages WHERE guid = ? LIMIT 1; - --- name: SelectPageBySlugAndSite :one -SELECT * FROM pages WHERE site_id = ? AND slug = ? LIMIT 1; - --- name: InsertPage :one -INSERT INTO pages ( - site_id, guid, title, slug, body, page_type, show_in_nav, sort_order, created_at, updated_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) -RETURNING id; - --- name: UpdatePage :exec -UPDATE pages SET - title = ?, - slug = ?, - body = ?, - page_type = ?, - show_in_nav = ?, - updated_at = ? -WHERE id = ?; - --- name: UpdatePageSortOrder :exec -UPDATE pages SET sort_order = ? WHERE id = ?; - --- name: DeletePage :exec -DELETE FROM pages WHERE id = ?; diff --git a/sql/schema/06_pages.up.sql b/sql/schema/06_pages.up.sql deleted file mode 100644 index 5090456..0000000 --- a/sql/schema/06_pages.up.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE pages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - site_id INTEGER NOT NULL, - guid TEXT NOT NULL, - title TEXT NOT NULL, - slug TEXT NOT NULL, - body TEXT NOT NULL, - page_type INTEGER NOT NULL DEFAULT 0, - show_in_nav INTEGER NOT NULL DEFAULT 0, - sort_order INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE -); -CREATE INDEX idx_pages_site ON pages (site_id); -CREATE UNIQUE INDEX idx_pages_guid ON pages (guid); -CREATE UNIQUE INDEX idx_pages_site_slug ON pages (site_id, slug); diff --git a/views/_common/nav.html b/views/_common/nav.html index e9c0de7..e8bce30 100644 --- a/views/_common/nav.html +++ b/views/_common/nav.html @@ -11,14 +11,11 @@ Posts - diff --git a/views/pages/edit.html b/views/pages/edit.html deleted file mode 100644 index d534b80..0000000 --- a/views/pages/edit.html +++ /dev/null @@ -1,55 +0,0 @@ -
    - {{ if .isNew }} -
    - {{ else }} - - {{ end }} - -
    -
    -
    - -
    - -
    - - {{ if not .isNew }} - - {{ end }} -
    -
    -
    -
    -
    Navigation
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    Page Settings
    -
    -
    - - -
    -
    -
    -
    -
    -
    - - {{ if not .isNew }} - - {{ end }} -
    diff --git a/views/pages/index.html b/views/pages/index.html deleted file mode 100644 index 3011c64..0000000 --- a/views/pages/index.html +++ /dev/null @@ -1,35 +0,0 @@ -
    -
    -
    - New Page -
    -
    - - {{ if .pages }} - - - - - - - - - - - {{ range .pages }} - - - - - - - {{ end }} - -
    TitleSlugNav
    {{ .Title }}{{ .Slug }}{{ if .ShowInNav }}Yes{{ end }}
    - {{ else }} -
    -
    No pages yet.
    -
    - {{ end }} -
    diff --git a/views/posts/edit.html b/views/posts/edit.html index b9f5ea7..d162788 100644 --- a/views/posts/edit.html +++ b/views/posts/edit.html @@ -11,12 +11,12 @@ -
    +
    {{ if $isPublished }} - + {{ else }} - - + + {{ end }}