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 bysort_order ASCSelectPage(id)— single page by IDSelectPageByGUID(guid)— single page by GUIDInsertPage— create new page, returns IDUpdatePage— update page fieldsDeletePage(id)— delete pageUpdatePageSortOrder(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/reordervia 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/)
Servicestruct with DB provider dependencyCreatePage(ctx, params)— generates GUID, derives slug from title if not provided, sets timestampsUpdatePage(ctx, params)— updates fields, setsupdated_atDeletePage(ctx, pageID)— deletes pageListPages(ctx)— returns all pages for the site from context, ordered bysort_orderGetPage(ctx, pageID)— returns single pageReorderPages(ctx, pageIDs []int64)— accepts ordered list of page IDs, updatessort_orderfor each (sort_order = index in list)
Handler (handlers/pages.go)
PagesHandlerstruct withPageService- Standard CRUD handlers following the existing posts handler pattern
Reorderhandler accepts JSON array of page IDs, callsReorderPages
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:
renderPagesruns as a sequential step aftereg.Wait()returns inBuildSite
Publisher changes
pubmodel.Sitegets a newPages []models.Pagefield- The publisher fetches all pages for the site via
SelectPagesOfSiteand 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.