Skip to content
HomePagesHomePages template kit
Menu

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. SlotItem is 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.

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}>.

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.