SpectreSkills
← All skills

cms-customizer

v1.0.0

Drive a CMS theme editor as a three-pane customizer over a live cross-origin preview — query-marker preview (not a cookie), a theme-scoped token, load-bearing data-section-id hooks, and both render paths sharing every resolver. Trigger when building or extending a theme editor, visual customizer, or in-canvas section inspector.

CMS & Page Builderscmscustomizerprevieweditor
Install
npx @spectre-apps/skills add cms-customizer

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

What it does

The customizer

The customizer is a three-pane chrome (top bar · left rail · canvas · inspector) around a live preview of the public renderer. It is not a second renderer. The canvas is the site, mounted in an iframe, and the editor talks to it over a typed message union.

That one decision is what makes "what I see is what publishes" true by construction. A second, editor-only render path will drift within a release — different spacing, different chrome, a section the picker offers that the site cannot paint.

Preview mounts off a query marker, not a cookie

The canvas iframe is cross-origin. A SameSite=Lax cookie is not sent on a cross-site iframe request. Mount preview off ?cms_preview=1 (plus a token), never off a cookie the iframe cannot see.

The preview token is scoped to the theme, not the path, and the theme id comes from verified claims — never a query param the iframe could rewrite. A path-scoped token makes every template switch a re-mint; a theme-scoped one lets the operator move between index, page layouts, and the sandbox without losing the session.

Shareable stakeholder links are a longer-TTL token on the same scheme, not a second mechanism.

Both render paths share every resolver

Same registry, same spacing emitter, same chrome resolution, same normalizeBundle. A third path must not be able to forget one. The preview bridge renders the very same components against an in-memory bundle the editor holds — that is how a setting keystroke paints before save.

data-section-id and data-block-id are load-bearing, not debug aids. The canvas resolves hover, selection, and drag through them. A component that forgets to stamp them is invisible to the editor even though the shopper can see it.

Chrome renders in the root layout

Header and footer are not body sections. They render in the site's root layout, which cannot see the route. Lift the pathname into a request header (middleware / proxy) so chrome styles can be computed per route and passed down.

Consequences, named:

  • Chrome is hoverable, clickable, and live-updating in the canvas, but not draggable — it sits outside the bridge's tree.
  • Chrome spacing has no breakpoint layer unless you add one on purpose; the default is non-responsive.
  • Overlay / transparent chrome is a flag computed from the leading body section (a media hero that asks to blend), not a second header type.

The rail, the inspector, the top bar

  • Rail lists Header / Template / Footer. Drag-to-reorder the template group only. Nested blocks accordion inside their section and cannot cross out of it. "Add section" is a two-pane picker: the catalog on the left, a manifest-derived wireframe on the right. Paste-from-clipboard sits atop the list when one is available.
  • Inspector is one rail that takes turns: section settings, block settings, template settings (variant + assignment + preview instance), theme tokens. A missing per-type form is never a dead end — fall back to the field list the manifest already describes.
  • Top bar consolidates rather than growing one control per capability: a preview cluster (viewport × zoom), an icon strip (undo / comments / AI), a save-state glyph, an overflow for Preview / View live / History / Shortcuts, and a save action whose shape depends on whether the theme is live.

Publishing the live theme is saving it

On the live theme, Save is Publish — the public site renders the live theme's draft. On a draft theme, Save persists privately and Publish is a separate action (a split button). The editor learns which case it is in from liveThemeId === id, not from a status string the operator could desync.

Undo is a local timeline over bundle snapshots, not a server revision, until you add one. ⌘Z / ⌘⇧Z. The canvas is the feedback for bundle edits — toast only what the operator cannot see (a refused paste, a failed save). Validation stays inline.

The customizer inherits the operator's lighting

It neither pins a theme nor redefines colour tokens. A private dark palette inside the editor is how it froze out of the dashboard twice. The editor scope survives for density rules and as the boundary a theme's --cms-* tokens can never cross.

The one colour literal that is allowed to stay is the canvas frame's paper: that is the previewed page, not admin chrome, and it must not follow the operator's dark-mode toggle.

Pair with cms-theme-contract for the document the editor mutates, and cms-publish-and-seam for what Save-on-live actually writes.