weiro/docs/superpowers/specs/2026-03-22-pages-design.md
Leon Mika a00567a756 Add arbitrary pages feature design spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 16:37:47 +11:00

5.1 KiB

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

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

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:

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.