Skip to content
HomePagesHomePages template kit

missing-slot-marker

"Every editable slot, every per-item collection row, and every declared row field must reach the rendered DOM with a selectable marker."

Venue: check — Every editable slot, every per-item collection row, and every declared row field must reach the rendered DOM with a selectable marker.

check renders every one of the section’s fixtures, parses each result, and reads the markers that actually reached the DOM. It reports three absences:

  1. A slot never marked, or marked only non-selectably. Any editable slot — one that does not declare locked: true — appearing as data-slot-id in no fixture’s rendered tree, or appearing only on elements flagged selectable={false}. A flagged copy keeps identity and still receives patches, but it satisfies no coverage requirement: every slot needs at least one selectable rendering.
  2. A per-item collection row never marked. In each fixture, every index 0…n-1 of the fixture’s array must appear as data-slot-item on a selectable element — and each rendered copy of the collection must itself mark every row, because the editor targets every live copy. A repeated index is never an error: within one copy it is the same row rendered twice (a tripled carousel track), across copies the same collection rendered twice (a responsive tree), and both are live.
  3. A declared row field never marked. Every key of a record item’s fields record must appear as data-slot-field on some selectable row element, in some fixture.

Which lists are checked for rows is the declared item: a record item is edited per row, and a single-slot item is edited as one card. A unit-edited list requires no item markers — and must emit none: one rendering data-slot-item is reported as unit-collection-item-marker. Row fields remain a record-item concern only, since no other item declares fields.

The evidence is the rendered page, not the source text. That matters because the marker is normally emitted by a primitive — <Slot.Text slot={slot.headline} as="h1" /> — and no source scan can tell whether that element reached the DOM, or which attribute it landed on once it did. What the primitive emits is a real attribute on a real element, which is exactly what this check reads.

Three consequences worth naming:

  • Coverage is unioned across every fixture, not just typical. The set is typical, each authored states fixture, and one null fixture per declared slot — so a slot that only renders under a non-default variant case is legitimately absent from the baseline, and is reported until a states fixture selects that case with .at(layout.list).
  • The union has one exception: a slot’s own empty fixture. Every declared slot gets a generated null_<slot> fixture that renders it empty, and in that fixture that slot must still reach the DOM on at least one selectable element — both halves are judged in the fixture alone. Otherwise a slot hidden when its value is empty, or one whose blank state renders only selectable={false} copies, would pass on the strength of the fixtures where it does have a value — and an unfilled slot the user cannot click is a slot they can never fill. Every other slot in that same fixture is still judged by the union.
  • Where you write the marker is entirely up to you. A section-local component, a responsive branch, a .map() over rows — if the attribute lands in the DOM, the slot is marked. Equally, a marker on an element the section never renders does not count.

Carve-outs:

  • A slot declaring locked: true needs no marker. It cannot be selected in the editor, so there is nothing to mark. locked carries no default: absent MEANS editable, so every slot that says nothing is judged here.
  • A slot whose section cannot render at all is not reported here. With no rendered output there is no evidence either way, so the section is reported as section-unrenderable instead, alongside the id naming the underlying fault (bundle-incomplete, schema-invalid, fixtures-invalid).

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.

Proving it from the rendered tree rather than from source also closes a gap a source scan leaves open: a marker written in a branch that never executes, or on an element a conditional drops, reads as “marked” in the file and is absent from the page.

Bind the section’s schema once at the top of the render with bindSlots, then render each slot through the Slot.* primitive for its kind. The primitive owns the marker, so it lands on the element that renders the value and cannot be forgotten:

<Slot.Text slot={slot.headline} as="h1" />

Every primitive emits its binding’s own marker attribute on the element it renders: data-slot-id for a top-level slot, data-slot-field for a field of a record row. The same Slot.* components serve both — they spread whichever attribute the binding they were handed carries. Slot.Text / Slot.Number add data-slot-text-leaf on a raw render — no format, no children — which is what lets the editor patch that element’s text on each keystroke instead of waiting for the next server render. For a collection slot, Slot.List marks the container and stamps data-slot-item on each row with the row’s array index, since that index is the address the editor patches through.

Slot.Image / Slot.Video mark every branch they can render, the empty state included — so <Slot.Image slot={slot.hero} /> is marked whether or not the slot declares a frame, and whether or not it has a value. The marker rides the framed wrapper when the slot declares a frame, and the single element the unframed branch renders when it does not — the <img>, the <video>, the provider <iframe> a link-sourced video renders, or the placeholder div standing in for whichever of those is missing. A responsive <img> keeps the marker even though a <picture> wraps it.

The primitive is the only route to a marker: a hand-written data-slot-* attribute is reported by no-slot-marker-literal, because a mistyped key there becomes a marker that silently never reaches the DOM — and no hand-written marker adds data-slot-text-leaf, which only a primitive’s raw render emits.

A decorative second copy the editor should never resolve a click into renders with selectable={false}: the copy keeps its full marker identity and still receives patches (so it never lags a mirror), but selection, outlines and scroll pass it by. A flagged copy counts toward no coverage requirement — if a slot is reported here, check that at least one of its renderings is selectable.

If the slot genuinely is not meant to be edited, say so in the schema rather than leaving the check unsatisfied: locked: true.

sections/hero/section.ts
import { defineSchema, type SectionProps, text } from "@homepages/template-kit";
export const schema = defineSchema({
label: "Hero",
slots: {
headline: text.medium({ label: "Headline" }),
eyebrow: text.short({ label: "Eyebrow" }),
},
});
export type Props = SectionProps<typeof schema>;
// sections/hero/Renderer.tsx — template-kit/missing-slot-marker: eyebrow
import { bindSlots, Section, Slot } from "@homepages/template-kit";
import { schema } from "./schema.js";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) {
const slot = bindSlots(schema, slots);
return (
<Section>
{/* bound, but rendered as a plain <p>: no primitive, so no marker */}
<p className="text-ink-mute">{slot.eyebrow.value}</p>
<Slot.Text slot={slot.headline} as="h1" />
</Section>
);
}
sections/hero/Renderer.tsx
import { bindSlots, defineSchema, Section, Slot, text } from "@homepages/template-kit";
import type { SectionProps } from "@homepages/template-kit";
const schema = defineSchema({
label: "Hero",
slots: {
headline: text.medium({ label: "Headline" }),
eyebrow: text.short({ label: "Eyebrow" }),
},
});
type Props = SectionProps<typeof schema>;
export function Renderer({ slots }: Props) {
const slot = bindSlots(schema, slots);
return (
<Section>
<Slot.Text slot={slot.eyebrow} as="p" className="text-ink-mute" />
<Slot.Text slot={slot.headline} as="h1" />
</Section>
);
}

If eyebrow was never meant to be editable, the other legitimate fix is in the schema — and then no marker is needed:

eyebrow: text.short({ label: "Eyebrow", locked: true }),
  • The marker contract — why the markers exist and what the editor reads them for.
  • The marker attributes — every marker attribute and which primitive emits it.
  • sidebar-order — the other rule proven from the rendered tree: the order those same markers appear in must match the schema.
  • group-ancestry — a marked slot that belongs to a group must also sit inside that group’s element.
  • section-unrenderable — what is reported when the section produces no rendered output for any of these checks to read.