Skip to content
HomePagesHomePages template kit

`Renderer.tsx` props

The `Renderer.tsx` declaration — props derived from the schema, what an `ImageValue` carries, and the purity rule every renderer is held to.

Never hand-write your prop type. Derive it, and the schema stays the single source of truth. bindSlots ties that same schema to this render’s values, and each Slot.* primitive renders one binding and owns its marker — see Slot primitives for the whole surface.

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" }) },
});
type Props = SectionProps<typeof schema>;
export function Renderer({ slots, nav }: Props) {
const slot = bindSlots(schema, slots);
// headline is `string` — a text slot's canonical blank is "", never null
return (
<Section id={nav.selfAnchor}>
<Slot.Text slot={slot.headline} as="h1" />
</Section>
);
}

The schema import here is a value, not import type: a binding carries the slot’s declared def, which is how Slot.Image and Slot.Video read the slot’s own frame and playback instead of having them re-passed at the call site.

SectionProps reads the schema literal and resolves each slot to its runtime value shape: a text becomes string (an unfilled optional arrives as the blank ""), an optional number becomes number | null, an image becomes ImageValue, a list an array, and so on. A slot you did not declare is a compile error to read.

An ImageValue splits into fields a renderer reads and fields it only carries. Hand the whole value to Image, which reads url, alt, responsive, and mobile off it. Ignore rect, ar, and crop_key: they are the editor’s re-edit state (so its cropper can reopen at the last crop) and the provenance of the already-cropped url. Framing is baked into the asset the platform serves, never applied at render time — an uncropped image is a centered object-cover over the original and a cropped one is a pre-cropped derivative, so there is no framing for a renderer to apply. A slot’s crop config steers the editor’s cropper, not your markup.

The markup a section is required to emit — the section root, the editor’s slot markers — comes from the kit’s primitives; everything else is your own JSX. See Slot primitives.

Renderers must be pure functions of props — no fetches, no globals, no useEffect for data, no Date.now() / Math.random(). They are rendered to static markup by the publisher and re-mounted constantly by the editor.