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:
- A slot never marked, or marked only non-selectably. Any editable slot — one
that does not declare
locked: true— appearing asdata-slot-idin no fixture’s rendered tree, or appearing only on elements flaggedselectable={false}. A flagged copy keeps identity and still receives patches, but it satisfies no coverage requirement: every slot needs at least one selectable rendering. - A per-item collection row never marked. In each fixture, every index
0…n-1of the fixture’s array must appear asdata-slot-itemon 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. - A declared row field never marked. Every key of a record item’s
fieldsrecord must appear asdata-slot-fieldon 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 istypical, each authoredstatesfixture, 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 astatesfixture 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 onlyselectable={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: trueneeds no marker. It cannot be selected in the editor, so there is nothing to mark.lockedcarries 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-unrenderableinstead, alongside the id naming the underlying fault (bundle-incomplete,schema-invalid,fixtures-invalid).
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.
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.
Before
Section titled “Before”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: eyebrowimport { 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> );}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 }),See also
Section titled “See also”- 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.