SpectreSkills
← All skills

cms-imported-sections

v1.0.0

Turn a customer design (Figma, HTML, a Claude Design export) into an editable CMS section that is DATA, not code — a tag-allowlisted template tree, CSS scoped on write, types namespaced custom:<id>, snapshotted at publish. Trigger when importing a design into a page builder, building a section importer, or deciding how user-authored sections render.

CMS & Page Builderscmsimportfigmacustom-sections
Install
npx @spectre-apps/skills add cms-imported-sections

Writes .claude/skills/cms-imported-sections/SKILL.md. Add --user to install globally.

What it does

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.owner is 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 section hero cannot shadow yours — for them or for anyone else. One flat registry stays one flat registry; see cms-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 css already carries its [data-section-type="…"] prefix — do not scope it twice. :root / html / body are rewritten to the scope rather than dropped, because a converted design very often sets its custom properties there and dropping that rule takes every var() 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-templates and cms-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 dangerouslySetInnerHTML on the render path, and there must never be one.

Inputs

The converter accepts three sources and produces the same definition:

SourceWhat you hand it
Claude Design exportReal markup + CSS to port — lift structure, remap colors to tokens
Figma frameStructured layer tree + text content, or a screenshot if that is all you have
Pasted HTMLThe 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.