Skip to content
HomePagesHomePages template kit
Menu

template-kit/props-from-schema

The template-kit/props-from-schema authoring rule — what it enforces, why, and how to fix it.

A Renderer’s props type is derived from schema.ts, never hand-written in the Renderer.

The rule runs only on Renderer.tsx. It reports an exported type alias or interface whose name ends in PropsProps, HeroProps, GalleryProps.

Two consequences of that being the whole test:

  • It does not read the right-hand side. Even export type Props = SectionProps<typeof schema>; reports if you write it in the Renderer. The derivation belongs in schema.ts; the Renderer imports it.
  • A non-exported local type is invisible to the rule. An internal type Row = { id: string }; used to type a helper’s parameter is your business, and is not reported.

import type { Props } from "./schema.js"; is the shape the rule is steering you to, and it reports nothing.

The schema is the section’s single source of truth, and the prop type is inferred from itSectionProps<typeof schema> reads the schema literal and resolves each slot to its runtime value shape: a required text becomes string, an optional one string | null, an image becomes ImageValue, and so on.

A hand-written Props shadows that inference, and then drifts silently. Add a slot to schema.ts and forget to render it: with the derived type, reading slots.subhead in the Renderer is a compile error until the slot exists, and the platform’s tooling and your renderer agree by construction. With a hand-written Props, the Renderer keeps compiling happily against the old shape — while the editor, the fill pipeline, and the schema all now believe in a slot your markup has never heard of. Nothing fails until a user is looking at a page with a missing block in it.

It also forces the editor to reason about a second definition of the same thing.

Declare the derived type once in schema.ts and import it in the Renderer:

export type Props = SectionProps<typeof schema>;
import type { Props } from "./schema.js";
sections/hero/Renderer.tsx
import { Section, Slot } from "@homepages/template-kit";
export type Props = {
slots: { headline: string };
};
export function Renderer({ slots }: Props) {
return (
<Section>
<Slot id="headline" as="h1" textLeaf>
<span>{slots.headline}</span>
</Slot>
</Section>
);
}
sections/hero/schema.ts
import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
export const schema = {
meta: { displayName: "Hero" },
slots: {
headline: { type: "text", size: "medium", required: true, produced_by: "headline_text" },
},
} as const satisfies SectionSchemaShape;
export type Props = SectionProps<typeof schema>;
sections/hero/Renderer.tsx
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>
</Section>
);
}

The as const satisfies SectionSchemaShape is load-bearing: without it the literal types are widened away and SectionProps can no longer tell a required slot from an optional one.

  • Schema system — the schema literal, SectionProps inference, and the four contract files.