template-kit/slot-marker-literal
The template-kit/slot-marker-literal authoring rule — what it enforces, why, and how to fix it.
A slot key must be written as a string literal wherever it is marked.
SlotItemis exempt.
These four places carry a slot key, and each must be a string literal:
| Where | Example |
|---|---|
<Slot id> |
<Slot id="headline"> |
<SlotGroup id> |
<SlotGroup id="agent"> |
slotId on any element |
<Image slotId="hero_image" …> |
data-slot-id / data-slot-group on any element |
<div data-slot-id="headline"> |
Anything that is not a plain string literal reports: an expression (<Slot id={key}>), a
template literal (<Slot id={`slot_${n}`}>), and a valueless attribute
(<div data-slot-id>).
SlotItem is deliberately exempt. Its prop is index: number — an array position,
written index={i} inside a .map(), not a schema key. There is nothing to cross-check
it against, so an expression there is correct and expected.
Reason
Section titled “Reason”This is not style. A separate, cross-file completeness check verifies that every slot
declared in schema.ts has a marker in the section’s JSX — and it reads these keys
statically, from the source text, without running your Renderer.
A key written as an expression is a key it cannot resolve. So the check cannot see the slot as marked, and — worse — the marker that actually reaches the browser is whatever your expression happened to evaluate to. The slot silently loses editor selection: it still fills, it still renders, and a user clicking on it in the editor selects nothing. Nothing throws. The failure is invisible until someone tries to edit that block.
This rule exists so that missing-slot-marker can work. A
.map()-generated slot key defeats both at once: the completeness check reports the slot
as unmarked, and no lint tells you why.
This shows up in two different shapes, and the fix depends on which one you’re in.
A key that is static but not literal — held in a const, or assembled with a
template literal — isn’t generating anything: there is exactly one possible value, so
unwrap it back into a plain inline string literal.
A key generated inside a .map() over an array is a genuine collection: declare it
as one slot in schema.ts, mark the array once with <Slot>, and mark each row with
<SlotItem index={i}>.
Before
Section titled “Before”import { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
const FIELD = "headline";
export function Renderer({ slots }: Props) { return ( <Section> <Slot id={FIELD} as="h1" textLeaf> <span>{slots.headline}</span> </Slot> <Slot id={`sub${"title"}`} textLeaf> <p>{slots.subtitle}</p> </Slot> </Section> );}Neither FIELD nor the template literal is doing any work — both spell out their whole
value at the call site. That’s the common case: a constant or a template literal standing
in for a string that was always going to be one string.
import { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) { return ( <Section> <Slot id="headline" as="h1" textLeaf> <span>{slots.headline}</span> </Slot> <Slot id="subtitle" textLeaf> <p>{slots.subtitle}</p> </Slot> </Section> );}The other shape is different: a key actually produced by iterating an array is modelling a real collection, and the fix is structural, not a matter of unwrapping.
// Before — `.map()` produces a different key each iteration; none of them is a literal.const FIELDS = ["headline", "subhead"] as const;
export function Renderer({ slots }: Props) { return ( <Section> {FIELDS.map((key) => ( <Slot key={key} id={key} textLeaf> <p>{slots[key]}</p> </Slot> ))} </Section> );}A genuine collection keeps its loop — the literal key is on the <Slot>, and the loop
variable stays on SlotItem’s index, which is exempt:
<Slot id="amenities"> {slots.amenities.map((item, i) => ( <SlotItem key={item.id} index={i} as="li"> <h3>{item.label}</h3> </SlotItem> ))}</Slot>Don’t reach for this structural rewrite for the first case above — a named constant or a template literal is not a collection, and modelling it as one is solving a problem you don’t have.
See also
Section titled “See also”- Contract primitives — the marker attributes and which primitive emits each.
- Schema system — where slot keys are declared.