# 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.