Add arbitrary pages feature design spec
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3d8c6f5345
commit
a00567a756
148
docs/superpowers/specs/2026-03-22-pages-design.md
Normal file
148
docs/superpowers/specs/2026-03-22-pages-design.md
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
# 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.
|
||||||
Loading…
Reference in a new issue