Skip to content
HomePagesHomePages template kit

no-raw-slot-values

"Read slot values through bindSlots/bindItems, never off the raw `slots` prop."

Venue: eslint — Read slot values through bindSlots/bindItems, never off the raw slots prop.

A section reads slot values only through a binding. The rule reports, in any .ts or .tsx file:

  • a member read of the slots prop — slots.units, slots["headline"];
  • the same read spelled as destructuring — const { headline } = slots;
  • the undestructured form — props.slots.<name>.

Reading a slot definition off the schema (schema.slots.<name>) is not a value read and passes. So do nav, variant and every other section prop — this rule is about slot values only.

bindSlots and bindItems are the value boundary. What the schema promises, the binding delivers: every value is normalized to its kind’s canonical shape (a legacy raw number stored in a text slot arrives as a string; an unrecognizable value degrades to that kind’s blank), and each binding carries a derived present you can branch on.

The raw slots prop is that same data before any of it happens. A value read from it is whatever the content document holds — so the section has to defend itself, and that defense spreads. One section grew a hand-written string coercion it called at ten separate sites, because a single unguarded read anywhere in the file was enough to print [object Object] on a published page. Binding once at the top of the render is what makes those guards unnecessary, and this rule is what keeps them from coming back.

Bind once, at the top of the render, and read through the binding everywhere after.

import { bindSlots, Section, Slot } from "@homepages/template-kit";
import { schema } from "./section.js";
import type { Props } from "./section.js";
export function Renderer({ slots }: Props) {
const slot = bindSlots(schema, slots);
// template-kit/no-raw-slot-values: skips the binding layer
const units = slots.units;
return (
<Section>
<Slot.Text slot={slot.intro} as="p" />
<p>{units.length} homes</p>
</Section>
);
}
import { bindItems, bindSlots, defineSchema, list, number, Section, Slot, SlotGroup, text } from "@homepages/template-kit";
import type { SectionProps } from "@homepages/template-kit";
const schema = defineSchema({
label: "Floorplans",
slots: {
intro: text.long({ label: "Intro" }),
units: list.of(
{ label: text.short({ label: "Label" }), beds: number({ label: "Beds" }) },
{ label: "Units" },
),
},
});
type Props = SectionProps<typeof schema>;
export function Renderer({ slots }: Props) {
const slot = bindSlots(schema, slots);
const units = bindItems(slot.units);
return (
<Section>
<Slot.Text slot={slot.intro} as="p" />
<Slot.List slot={slot.units} as="ul">
{units.map((unit) => (
<SlotGroup slot={unit} as="li" key={unit.index}>
<Slot.Text slot={unit.fields.label} as="span" />
</SlotGroup>
))}
</Slot.List>
</Section>
);
}
  • Values and emptiness — what the binding guarantees, what present derives, and who owns formatting and the empty state.
  • Slot primitives — the primitive for every slot kind, and what each one does with a blank value.
  • no-slot-marker-literal — the identity half of the same boundary: markers come from the primitives, values come from the bindings.