Skip to content
HomePagesHomePages template kit

Read the starter

The three files of a section folder, the job each one owns, and why the sample data is not one of the other two.

templates/starter/sections/main/ is three files, and you have just watched what they produce. Read them in the order they depend on each other — what the section has, what it previews against, how it renders — and the whole authoring contract is in front of you. Nothing to run here; open the files beside this page.

import type { SectionProps } from "@homepages/template-kit";
import { defineSchema, text, textBlock } from "@homepages/template-kit";
// One text slot and the decision that fills it — the smallest section that
// renders, fills and checks. Declare more with the per-kind slot builders
// (`text.*`, `number`, `select`, `image`, `video`, `url`, `document`, `poi`,
// and `list.of(item, options)` for anything repeating).
export const schema = defineSchema({
label: "Main",
anchor: "main",
slots: {
// Everything about this slot in one declaration, fill included. `cap` is the
// slot's character budget and the only place it is written — the fill
// decision inherits it.
headline: text.medium({
label: "Headline",
cap: 80,
// How the AI fills this slot. Its id and `produces` are minted from the
// slot key. Restate the character budget in `content` — a brief that asks
// only for a sentence count overruns a tight cap, and an over-cap answer is
// trimmed, not retried forever.
fill: textBlock({
structure: "single_sentence",
perspective: "third",
voice: "Write a short, confident heading for this section.",
content: "Anchor the section's purpose in one sentence, under 80 characters.",
}),
}),
},
});
// Renderer prop type — derived from `schema`; do not hand-write it.
export type Props = SectionProps<typeof schema>;

Two exports, and both are read by the platform without your component ever running.

schema is what the section exposes: its label and anchor, and one slot named headline. text.medium({ … }) is a per-kind slot builder — one per slot type, each taking only the options its type has, so a size belongs to text and a crop frame to image. That declaration is the whole answer to “what content does this section have, and where does each slot’s value come from”; nothing else in the folder may add to it. See the schema reference for the full builder surface and Slots for the idea it rests on.

Props is derived from schemaSectionProps<typeof schema> — and is never hand-written. Deriving it is what keeps the schema the single source of truth: a text slot arrives as string (an unfilled optional is the blank ""), and a slot you did not declare is a compile error to read. See Renderer.tsx.

fill: is not a third export — it sits on the slot it fills, so one declaration answers both what the slot is and how it gets a value. textBlock states the shape of the writing wanted — a structure, a perspective, the voice, and what the sentence should be about. The character budget is not among them: the slot declares cap and the decision inherits it. Not every slot carries one: a slot whose value comes from the property’s own data declares source: instead — it is read, not written — and the next page adds one of each. See AI fill and the fill reference.

It reads its sibling first — import { schema } from "./section";, the one line elided below — and then does exactly one thing with them:

import { defineFixtures } from "@homepages/template-kit/fixtures";
// The section's sample data, and the only module that reaches
// `@homepages/template-kit/fixtures` — the dev-only golden scenario corpus.
//
// It lives in its own file for one reason: nothing in the published import graph
// imports it, so the corpus cannot reach the bundle a published section ships.
// Keep it that way — never import this module from `Renderer.tsx` or `section.ts`.
//
// Nullability coverage (each slot rendered empty) is derived from `schema`,
// so there is no fixture to author for it. Add a `states` entry only for a designed
// scenario — a content-length or cardinality branch the section renders differently.
export const fixtures = defineFixtures(
schema,
(scenario) => ({
base: scenario("luxuryMultiUnit", {
headline: "A typical headline that reads well in a short line",
}),
}),
);

scenario("luxuryMultiUnit", …) names one of the kit’s golden scenarios — a whole synthetic property, shared by every workspace — and authors only the slots nothing else can resolve. defineFixtures resolves the rest against that scenario the way the fill pipeline would. The key is base, which is the fixture the dev server serves under the runtime id typical; that is the mismatch the previous page ran into.

Why it is a file of its own. Nothing in the published import graph imports it: section.ts does not, Renderer.tsx does not, and template.ts reaches the section through section.ts. So the golden corpus behind @homepages/template-kit/fixtures has no path into the bundle a published section ships — not because you remember to keep it out, but because the module graph gives it nowhere to travel. The only way to break that is to import this module from one of the other two, which is why the comment says not to. See Fixtures and scenarios for the reasoning and fixtures.ts for the full declaration.

It imports its Props type and schema from ./section — the two lines elided below — and is otherwise a plain component:

import type { ReactNode } from "react";
import { bindSlots, Section, Slot } from "@homepages/template-kit";
// Main — a pure, server-rendered function of its props. `bindSlots` ties the
// schema to its values; each `Slot.*` renders one and marks it for the editor. For
// interactivity, add a "use client" island file beside this one.
export default function MainRenderer({ slots, nav }: Props): ReactNode {
const slot = bindSlots(schema, slots);
return (
<Section id={nav.selfAnchor} className="bg-background text-ink">
<div className="mx-auto max-w-3xl px-6 py-12">
<Slot.Text slot={slot.headline} as="h2" className="text-2xl font-semibold" />
</div>
</Section>
);
}

bindSlots(schema, slots) ties the schema to this render’s values, and each Slot.* primitive renders one binding and emits the marker the editor selects on — the [slot:headline] you saw in the render outline is that marker. Section supplies the section element and its anchor. Everything else in the file is ordinary React and ordinary classes; the markup and the layout are yours.

What it does not do is decide anything: no fetch, no browser global, no clock. It runs on the platform’s servers, for content you never see — see What you’re building for why that is the one constraint the rest of the guide is downstream of, and Islands for where interactive code goes instead.

main has one slot so that every part of the contract is present and none of it is buried — it is a floor to build on, not a demonstration of how a real section is built. See What you’re building for the production-scale example to scaffold, and add your own template beside the starter by copying one that already works.

Without looking back at the page, you can say which file declares what the section has, which supplies the content a preview renders, and which produces markup — and one reason the third declaration is not folded into the first.

From the workspace root, this returns exactly one file, templates/starter/sections/main/fixtures.ts:

Terminal window
grep -rl "@homepages/template-kit/fixtures" templates/

If it ever returns two, the second one is a published module that can now reach the dev-only corpus.

Add a slot — add two slots to main: one the platform fills from the property’s own data, one written by AI.