CMS Page Builder (Wave 1): block-based Pages content type #47
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/cms-page-builder"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a generic block-based Pages content type — non-technical admins compose pages from a fixed palette of blocks instead of freeform HTML. This is Wave 1 of the spec (build-order steps 1–8); Wave 2 (dynamic blocks) and Wave 3 (revisions, asset picker) are deferred as separate work.
What's included
1. Rich-text alignment — left/center/right controls in the shared
RichTextEditor(@tiptap/extension-text-align), stored astext-alignon the block node. Server sanitize allowlist updated to permit onlytext-alignviastyleonp/h1–h6.2.
pagestable + block registry — newpagestable (blocks JSON-as-text, grouped SEO metadata + layout/nav settings columns,published_at). Block registry scaffold on both sides driving renderer/editor/palette/validation from one entry each.3. Wave 1 blocks —
heading,rich_text,image,two_column(container, holds ordered arrays of leaf sub-blocks per side),cta,divider,quote. Each has a server validation schema + client renderer/editor.rich_textis sanitized on save via the sharedcleanBodyallowlist.4. Save-time validation — every create/update validates blocks against the registry (envelope = 5 reserved keys, registered-type, per-block schema, one-level nesting cap) and sanitizes them. Authoritative server gate; a direct API call can't bypass it.
5. Admin builder UI —
PagesAdminlist +PageBuildercanvas: block palette, per-block editor cards with show/hide, up/down + native drag reorder, Content/Settings tabs, SEO + layout/nav panels, publish/unpublish, protect + password-gated unprotect modal, draft preview, delete.6. Routing — public catch-all
/:slug(matched after all named routes) + reserved-slug list guarding named routes/API namespaces;/preview/:id/:tokenoutside the maintenance gate.7. Protected pages —
protectedcan be set true via normal PATCH but only cleared viaPOST /pages/:id/unprotect, which re-verifies the current admin's password server-side (independent of JWT, never logged). Protected pages can't be deleted. Audit-logged.8. Draft preview —
POST /pages/:id/previewmints a 1h token;GET /public/pages/:id/preview/:tokenrenders the current (possibly unpublished) state, ungated.All endpoints live under the existing
/api/v1(admin + public routers) and are added to Swagger.API surface (all
/api/v1)GET/POST /admin/pages,GET/PATCH/DELETE /admin/pages/:idPOST /admin/pages/:id/unprotect,POST /admin/pages/:id/previewGET /public/pages/:slug,GET /public/pages/:id/preview/:tokenVerification
published_atstamped on first publish, slug immutable, reserved/duplicate slugs rejected, protected→delete blocked, unprotect-via-PATCH blocked./preview/:id/:tokenshowed the draft banner and rendered via token with no session. Test pages cleaned up.Client builds clean (216 modules). The admin builder canvas itself was not visually verified in-session (no admin session in the test browser; passwords aren't entered by the agent) — it compiles clean and reuses the already-proven block editors + endpoints.
Notes
/api/v1(not a newv2) and block order is array position — both confirmed with @whitlocktech.nav_group/show_in_navare stored but not yet consumed by any site nav (per spec).🤖 Generated with Claude Code
swagger-output.json now documents GET/POST /admin/pages, GET/PATCH/DELETE /admin/pages/:id, POST /admin/pages/:id/{unprotect,preview}, and the public GET /public/pages/:slug + /public/pages/:id/preview/:token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>