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>
- PagesAdmin: list view of pages (title/slug/status/protected/updated) with
new/edit navigation and a View link to the live page.
- PageBuilder: full-page block canvas — palette (adds any registered block),
per-block editor cards with show/hide, up/down + native drag reorder, and
remove; Content / Settings tabs; SEO metadata + layout/nav settings panels;
publish/unpublish; protect (PATCH) and password-gated unprotect (modal);
draft preview (mints a token, opens /preview/:id/:token); delete (blocked
while protected). Surfaces server block-validation details on save.
- CmsPage: public renderer for /:slug (published; staff see drafts) and the
token-gated /preview/:id/:token, rendering blocks via BlockList and
reflecting the page title/meta.
- Routing: /:slug catch-all after all named routes + /preview/:id/:token
outside the maintenance gate; admin /admin/pages, /pages/new, /pages/:id.
- api client: public page/pagePreview + admin pages CRUD/unprotect/preview.
- AdminLayout: "Pages" nav entry (Content group) with icon.
- theme.css: builder canvas + preview-banner + shell-wide styles.
Client builds clean (216 modules).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend for the CMS page builder, all under the existing /api/v1:
- pages.model: authoritative save gate — validates blocks against the
registry and sanitizes them on every create/update; maps rows to/from the
grouped API shape (metadata / settings); slug validated + reserved-checked
at create and immutable after; `protected` can be set true via PATCH but
only cleared via the unprotect path; published_at stamped on first publish.
- sanitizeBlocks: post-validation normalizer (applies each block's sanitize,
stamps version, defaults visible, recurses container slots).
- reservedSlugs: guards page slugs from shadowing named routes/API namespaces.
- Admin routes (staff-gated): GET/POST /pages, GET/PATCH/DELETE /pages/:id,
POST /pages/:id/unprotect (password step-up, verified against the caller's
own hash, never logged), POST /pages/:id/preview (1h token). Audit-logs
create/publish/unpublish/protect/unprotect/delete.
- Public routes: GET /public/pages/:slug (published; staff see drafts; site-
mode gated) and GET /public/pages/:id/preview/:token (ungated, token is the
access control). Preview token primitives added to auth/token.js.
- Swagger annotations for all new endpoints.
Verified end-to-end: model integration test against the dev DB (sanitize,
invalid-block rejection, slug immutability, protected/unprotect, dup/reserved
slug, published_at) + authenticated HTTP smoke (201 create, 400 invalid
blocks, publish, public slug fetch, preview mint+fetch, 403 delete-protected,
401 wrong-password unprotect).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client block registry now carries a renderer, edit form, palette label/icon,
and defaults for all seven Wave 1 blocks (self-registering via
client/src/blocks/types/*): heading, rich_text, image, two_column, cta,
divider, quote.
- BlockRenderer + BlockList render stored blocks via the registry (respect
`visible`, tolerate unknown types), reading getBlock from ./registry to
avoid the index -> twoColumn -> BlockRenderer cycle.
- editorKit: shared Field/TextField/TextAreaField/SelectField styled with the
existing admin form classes; rich_text editor reuses RichTextEditor
(variant post), image editor reuses the shared uploader.
- two_column editor is a mini per-column canvas (add from the leaf-only
palette, edit via each block's registry editor, reorder, remove).
- theme.css: public block styles (heading/image alignment/cta/quote/
two-column responsive grid) + column sub-block editor styles.
Verified: all 11 modules transform cleanly under esbuild. Full visual
verification comes with the builder UI (step 5) + public route (step 6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Register all seven Wave 1 block types with their server-side validation
schemas, self-registering via server/src/blocks/types/*:
heading, rich_text, image, two_column (container), cta, divider, quote.
- propHelpers.js: shared validators (isSafeUrl rejects javascript:/data:/
protocol-relative, enum/required/optional text, strict key allowlist).
- rich_text carries a `sanitize` normalizer (registry now supports it) that
runs html through the shared cleanBody allowlist on save.
- Registry entrypoint requires the type modules so all schemas load.
Verified: all 7 register; valid blocks pass; malformed props yield precise
per-path errors; one-level nesting cap enforced; rich_text sanitize strips
script/onerror.
Client renderers + editors (step 3 client half) still to come.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New `pages` table: slug/title/blocks(JSON-as-text)/status/protected, author
FK, grouped SEO metadata + layout/nav settings columns (added up front per
spec — cheap now, painful to retrofit), published_at mirroring posts.
Block registry scaffold, server and client, defining the pattern without
any block types yet (Wave 1 lands in step 3):
- server/src/blocks: registry (register/get/list, reserved envelope keys,
container metadata) + validateBlocks (authoritative save-time gate:
envelope, registered-type, per-block schema, one-level nesting cap) +
index entrypoint that will register Wave 1 defs.
- client/src/blocks: mirror registry carrying renderer/editor/palette +
makeBlockId, plus index entrypoint.
Verified: schema applies idempotently against the dev DB (pages table +
indexes present); validator exercised for empty/non-array/unknown-type/
bad-envelope/duplicate-id/nested-container cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared RichTextEditor gains @tiptap/extension-text-align for heading and
paragraph nodes, serializing alignment as inline text-align on the block
node so it round-trips through save/reload. Fixed once at the shared
component so it also flows into the upcoming rich_text and two_column
page blocks.
Server sanitize allowlist now permits `style` on p/h1-h6, constrained by
allowedStyles to text-align (left/right/center/justify) only; all other
CSS properties and values are stripped.
Step 1 of the CMS Page Builder spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>