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.
section.ts
Section titled “section.ts”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 schema — SectionProps<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.
fixtures.ts
Section titled “fixtures.ts”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.
Renderer.tsx
Section titled “Renderer.tsx”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.
A skeleton, not a worked example
Section titled “A skeleton, not a worked example”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.
Verify
Section titled “Verify”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:
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.