Skip to content
HomePagesHomePages template kit
Menu

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: true must 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_user needs no marker. It cannot be selected in the editor, so there is nothing to mark. (editable_by_user defaults to false.)
  • 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 — and slot-marker-literal reports 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.

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.

sections/hero/schema.ts
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: eyebrow
import { 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>
);
}
sections/hero/Renderer.tsx
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 },