The section catalog
A first-party section is four artifacts, and missing one fails silently
(no registry entry → renders null; no preset → empty library card). Adding
a type means adding all four and satisfying the compiler everywhere they are
read — never editing six lists that can drift.
- Manifest — settings, blocks, CTAs,
spacingDefaults, a three-lettertag, apresetseeded with the design's real copy. - Component — reads settings with typed helpers; fallbacks equal the preset, so the site renders identically with no published theme.
- Registry entry — one flat
Record<type, Component>. Both the public renderer and the preview canvas dispatch through this same map. - Preset — real copy, not lorem. The library page and the Figma/reference capture render from it.
The picker, the section library, and the all-sections sandbox are all manifest-driven. A new manifest appears in every one of them for free. Do not add a fifth list.
Field-kind mapping
Map a design onto the SettingField union. Colors are global theme
tokens, not per-section settings, unless the value is genuinely local.
| Design element | Field |
|---|---|
| Editable line / paragraph | text (multiline: true for paragraphs) |
| Formatted prose | richtext |
| A link target | url |
| An image / video slot | image / video |
| On/off | checkbox |
| A fixed set of choices | select (segmented when ≤ 3 options) |
| A numeric knob | range (min/max/step/unit) |
| A visual group divider | header (label only, not an input) |
| A named menu | menu_picker (stores a handle) |
| A flat link list | links (JSON-encoded string) |
Repeating groups become blocks: a BlockManifest plus seeded preset
instances. Buttons become CTAs — a homogeneous sibling repeater to
blocks (every CTA shares one field set: label, link, style). Do not
hand-roll a primary/secondary pair in settings.
Anchor groups
A page anchor is a fixed section the page cannot do without (a hero, a
product buy box). When a page should offer a choice of layouts for the
same slot, the alternatives share an anchorGroup string. The reconciler's
rule becomes exactly one member of the group: extras drop (keeping the
first), and a page with none gets the group's default.
A layout swap starts the new anchor from its own preset. It deliberately does not carry the old anchor's blocks — they are different layouts, not skins.
Spacing is generated from the manifest
spacingDefaults on the manifest must mirror what the component
actually renders. The CSS fallbacks are generated from those numbers
(emitCatalogSpacingCss) and injected by the site's root layout. There is
one copy of each number.
Never hand-write a padding-block rule for a .cms-section--* class. Change
the manifest. If the two copies drift, every spacing control opens at a
value the page never had, and Reset breaks the design.
Two mechanisms, deliberately different:
- Sections emit custom properties with CSS-side fallbacks.
- Blocks emit concrete values with
!important, only for edges the operator actually set.
Adding a section: the whole list
Work top to bottom. A missed touchpoint is the silent failure.
- Manifest —
type(kebab-case, globally unique),name,description,tag,scope, settings/blocks/CTAs,spacingDefaults, a preset whose copy is distinct (two settings that render the same string break content-keyed locators). - Component — fallback defaults ≡ preset. Read with
str/num/boolhelpers; render CTAs through the shared CTA helper, not ad-hoc buttons. - Registry — add the type to the flat map. This is the #1 silent miss.
- CSS — a root class plus nested kebab-case classes; theme tokens, never a raw hex where a token exists. Section padding comes from the generated custom properties.
- Default theme — add the type to a template only if it should ship in the unpublished fallback theme.
- Verify — typecheck the contract package, regenerate any lock the catalog is gated on, open the section library / all-sections sandbox, and compare the card against the source design.
A human reviews the rendered result. This skill never publishes a theme.
Three sections that carry decisions
- Operator HTML (
custom-html) strips<script>, inlineon*handlers, andjavascript:URLs. That covers an embed container hydrated by a separately-loaded app script. Do not loosen the strip to support raw script embeds — add a vendor allowlist or a CSP nonce, so the audit trail survives. - Lead form POSTs to an operator-supplied
action. With none set, submit is disabled. The CMS stores no submissions of its own; wiring it to a forms product needs no manifest change. - Countdown stores an ISO instant, never a duration, and its digits are a client component. Under ISR a server-rendered clock freezes at generation time and shows every visitor the same wrong number.
Pair with cms-imported-sections when the section is arriving as customer
data rather than as four artifacts in the repo.