CMS page builder
Everything needed to take a visual page builder from an empty pair of apps to something people can design a real site in: the theme contract, the section catalog, the importer, pages-as-content, the customizer, the publish seam, and tenancy. Install the skills and let Claude work through them in order.
This is the architecture behind Spectre CMS (and the Mission Control storefront CMS it was ported from). The skills are the invariants, not the file paths — use them to build the same shape, or to extend one that already has it.
Order
- cms-theme-contract —
{ settings, templates, sectionGroups }as one JSON document of primitive setting values, normalized on every read, in a package that imports nothing. Sectiontypeis globally unique. Everything else is downstream of this. - cms-section-catalog — one catalog the picker, the library, the sandbox and both renderers read: four artifacts per type (manifest, component, registry, preset), blocks vs CTAs, spacing generated from the manifest, and the checklist for adding one.
- cms-imported-sections — a customer design becomes DATA. The
template is a tag-allowlisted tree, not an HTML string; types are
namespaced
custom:<id>; CSS is scoped on write; definitions snapshot at publish; archive, never delete. The field schema is the product, not the markup. - cms-pages-and-templates — a page is a workspace-scoped row.
The theme supplies only the layout.
resolvePageVariantfalls back todefault, so a theme swap never 404s a live URL. Saving a page is publishing it. - cms-customizer — the public renderer in an iframe, mounted off
a query marker (a cookie will not arrive cross-origin), talking
over a typed message union through
data-section-id. Publishing the live theme is saving it. - cms-publish-and-seam — the site never touches Postgres. The ISR cache key carries the host, because a shared key is one customer's site on another's domain. Versions are append-only; the snapshot is the bundle, not the pages.
- cms-tenancy — the workspace is yours; Clerk authenticates a
person. Resolution never creates a workspace. The cookie is a
hint.
FREEis a sentinel. Past due keeps the site up. The webhook is the only writer of billing state, and entitlement gates the write.
Notes
Two invariants are worth stating before any code:
- The public renderer and the customizer canvas are the same program. A second, editor-only render path will drift. Share the contract package, the registry, and every resolver.
- A bundle that cannot be parsed is a site that silently stops
rendering. That is why settings stay primitives, why
normalizeBundleruns on every read forever, and why an imported section that cannot be vouched for loses one node rather than the import.
The pieces interlock more tightly than most bundles. The contract
in step 1 is what step 5 mutates and what step 6 freezes. The
catalog in step 2 is what step 3 must never shadow (custom:<id>).
The page-as-row split in step 4 is why step 6's snapshot can omit
pages without 404ing a live URL. The host-keyed cache in step 6 is
the other half of the tenancy rules in step 7.
Steps 1, 2, 3 and 4 belong in a zero-dependency package both your API and your renderer import. That is what makes "the canvas is what publishes" true by construction rather than by maintenance.