template-kit/missing-slot-marker
The template-kit/missing-slot-marker authoring rule — what it enforces, why, and how to fix it.
Every slot declared
editable_by_user: truemust be marked in the section’s JSX. An unmarked slot still fills and still renders — it just cannot be clicked.
For each slot in schema.ts with editable_by_user: true, the section’s JSX must carry a
marker whose key is that slot id, written as a string literal. Any of these counts:
| Marker | Example |
|---|---|
<Slot id> |
<Slot id="headline" textLeaf> |
slotId on a primitive that emits its own marker |
<Image slotId="hero_image" …> |
a raw data-slot-id attribute |
<div data-slot-id="headline"> |
The scan covers every .tsx in the section folder, not only Renderer.tsx — a marker
inside a section-local component satisfies it. Test files are excluded.
Carve-outs:
- A slot that is not
editable_by_userneeds no marker. It cannot be selected in the editor, so there is nothing to mark. (editable_by_userdefaults tofalse.) - A computed key does not satisfy the rule.
<Slot id={fooKey}>or an id produced inside a.map()is not a literal, so the check cannot resolve it — andslot-marker-literalreports it separately. A.map()-generated key therefore fails both at once.
The practical corollary of the two above: when you hoist markup into a section-local
helper, do not pass the slot key through it as a prop. <Field slotKey="headline">
wrapping an inner <Slot id={slotKey}> erases the literal from the source entirely, and
the slot reads as unmarked. Keep the literal <Slot id="…"> where the slot is rendered
and move only the data layer and leaf presentation out.
Reason
Section titled “Reason”The markers are the only thing connecting the pixels a user clicks to the schema key
behind them. The editor’s click resolver walks up from the clicked node looking for a
data-slot-id; a slot that emits none is invisible to it.
Nothing throws. The slot is filled by AI, it renders its content on the published page, and it looks completely correct — right up until a user clicks it in the editor and selects nothing. That is the whole failure: fillable but not editable, discovered by a customer rather than by you.
Wrap the slot’s outer DOM in <Slot id="…"> — add textLeaf for a single-leaf text slot
so the editor can write to it on each keystroke. An image slot needs no wrapper: give
<Image> its slotId and it emits the marker itself.
A raw data-slot-id attribute is not a lesser fallback — for the marker itself, it’s
identical to what <Slot> renders: <Slot as="p" id="…"> emits exactly <p data-slot-id="…">, nothing more. The one thing <Slot textLeaf> adds is
data-slot-text-leaf on the child, wired automatically, so the editor can patch text on
every keystroke instead of waiting for the next server render. So default to <Slot> for
the convenience — it’s one prop instead of hand-wiring a second attribute — but writing
data-slot-id directly on markup you’re already hand-assembling is a legitimate, idiomatic
fix, not a technicality; reach for it freely when you don’t need textLeaf.
If the slot genuinely is not meant to be edited, say so in the schema rather than leaving
the check unsatisfied: editable_by_user: false.
Before
Section titled “Before”import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
export const schema = { meta: { displayName: "Hero" }, slots: { headline: { type: "text", size: "medium", required: true, editable_by_user: true }, eyebrow: { type: "text", size: "short", required: false, editable_by_user: true }, },} as const satisfies SectionSchemaShape;
export type Props = SectionProps<typeof schema>;// sections/hero/Renderer.tsx — template-kit/missing-slot-marker: eyebrowimport { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <Section> <p className="text-ink-mute">{slots.eyebrow}</p> <Slot id="headline" as="h1" textLeaf> <span>{slots.headline}</span> </Slot> </Section> );}import { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <Section> <Slot id="eyebrow" textLeaf> <p className="text-ink-mute">{slots.eyebrow}</p> </Slot> <Slot id="headline" as="h1" textLeaf> <span>{slots.headline}</span> </Slot> </Section> );}If eyebrow was never meant to be editable, the other legitimate fix is in the schema —
and then no marker is needed:
eyebrow: { type: "text", size: "short", required: false, editable_by_user: false },See also
Section titled “See also”- Contract primitives — every marker attribute and which primitive emits it.
slot-marker-literal— why the key must be a literal for this check to see it at all.slot-group-marker— the same idea for a group of slots.