Imported sections
A first-party section is four artifacts, and three of them are code. That is fine for a catalog you ship and impossible for one a customer imports at 11pm. So an imported section is DATA, and the renderer interprets it.
This is the product. Everything else in a page builder is table stakes against Webflow and Framer; "the design your team already made, editable, in a minute" is not.
The template is a tree, not an HTML string
That is the entire security argument:
- no parser on the render path
- no
dangerouslySetInnerHTML - no sanitiser to keep current against the next mutation-XSS technique
- no way for a template to express a
<script>
The tag allowlist is a TypeScript union, so an unknown tag fails at IMPORT, once, rather than at render, forever. It is also the format a language model emits most reliably, because every node is a small closed choice rather than free text.
form / input / iframe / script / style / every SVG element are
absent, and absent is the default — a tag nobody thought about is refused
rather than allowed. A section that collects data belongs to a first-party
lead-form (which has a submission path and an audit trail). An imported
form would have none of those and would look identical to a customer.
normalizeSectionDefinition is the parse boundary and it treats its input
as hostile: the input is a conversion of a customer's design, produced
by a model, and neither of those is trusted. It coerces forward and drops
what it cannot vouch for rather than throwing — one bad node loses one
node, not the import.
The field schema is the product, not the markup
Deciding that a design has a heading, a subheading, an image, and a repeating three-card block is what turns a picture into something a marketer can edit. A conversion that returns one rich-text blob is technically a success and commercially worthless.
Spend the converter prompt on that judgment. Land imports as drafts. The review screen shows the derived fields, not a thumbnail. A wrong schema is fixed by re-importing until a field editor exists; do not ship an importer that writes straight to live.
Bound the input. A pasted design is a few hundred KB at most; anything larger is a whole page or a mistake, and both cost real money to feed to a model before failing.
Seven decisions that hold the line
manifest.owneris the tenant. Threaded through every enumeration surface (picker, library, schema API, default theme). Missing one leaks a second catalog.- Types are namespaced
custom:<id>. A customer naming their sectionherocannot shadow yours — for them or for anyone else. One flat registry stays one flat registry; seecms-theme-contract. - The interpreter is ONE registry entry. Both render paths dispatch through the same map. There is no third path that could forget it, and an imported section is not compiled into the first-party map (that map is a module constant; imported sections arrive per workspace at runtime).
- CSS is scoped on the way IN, not at render. The stored
cssalready carries its[data-section-type="…"]prefix — do not scope it twice.:root/html/bodyare rewritten to the scope rather than dropped, because a converted design very often sets its custom properties there and dropping that rule takes everyvar()with it. - Definitions are SNAPSHOT into the published version, beside the
compiled CSS. A rollback must restore last week's section markup, not
today's. Contrast with pages, which are not snapshot at all: a section
definition is Theme, page copy is Content. See
cms-pages-and-templatesandcms-publish-and-seam. - Archive, never delete. A published version references the section by type and is immutable; deleting the row leaves a rollback rendering a page with a hole in it.
- No path from a definition or a setting value to executable markup.
There is no HTML parser and no
dangerouslySetInnerHTMLon the render path, and there must never be one.
Inputs
The converter accepts three sources and produces the same definition:
| Source | What you hand it |
|---|---|
| Claude Design export | Real markup + CSS to port — lift structure, remap colors to tokens |
| Figma frame | Structured layer tree + text content, or a screenshot if that is all you have |
| Pasted HTML | The same tree the model would emit, already closer to the output |
What it actually produces is the field schema. The markup is the easy half.