A section is the unit of content a Kotao page is built from. Each section has a type (e.g. hero, text_with_image, product_grid), a schema (the typed shape of its data), and a renderer (your theme’s Astro component for that type).
The registry
@kotao/sites-section-schemas exports SECTION_REGISTRY — a frozen map of every section type the runtime understands, keyed by type, with its Zod schema as the value:
import { SECTION_REGISTRY } from "@kotao/sites-section-schemas";
console.log(Object.keys(SECTION_REGISTRY));
// → ["hero", "text_with_image", "grid", "product_grid", "blog_list", ...]
The registry is the contract between the editor, the runtime, and your theme. The editor reads it to know which field controls to render for each section. The runtime reads it to validate stored data on every render. Your theme reads it (via defineTheme()) to verify it has a renderer for every type.
Field shapes
Each schema is a Zod object. A simplified hero looks like:
import { z } from "zod";
export const heroSchema = z.object({
heading: z.string().min(1).max(200),
subheading: z.string().optional(),
cta: z
.object({
label: z.string(),
url: z.string().url(),
})
.optional(),
alignment: z.enum(["start", "center"]).default("center"),
});
In your renderer, the data prop is typed against this schema:
---
import type { SectionRendererProps } from "@kotao/sites-theme-sdk";
interface Props extends SectionRendererProps<"hero"> {}
const { data } = Astro.props;
// data.heading → string
// data.subheading → string | undefined
// data.cta → { label, url } | undefined
// data.alignment → "start" | "center"
---
You don’t have to re-validate data in your renderer — the storefront has already validated it against the schema before handing it to you. Anything that reaches your component is type-safe.
Adding a section type to your theme
Three steps:
1. List it in theme.json
{
"sections": ["hero", "text_with_image", "grid", "newsletter"]
}
2. Create the renderer
src/sections/newsletter/Section.astro
---
import type { SectionRendererProps } from "@kotao/sites-theme-sdk";
interface Props extends SectionRendererProps<"newsletter"> {}
const { data } = Astro.props;
---
<form method="post" action={`/api/forms/${data.formId}`}>
<label>
<span>{data.label}</span>
<input type="email" name="email" required />
</label>
<button type="submit">{data.submitLabel}</button>
</form>
3. Wire it in src/theme.ts
import Hero from "./sections/hero/Section.astro";
import Newsletter from "./sections/newsletter/Section.astro";
export default defineTheme({
// …
sections: {
hero: Hero,
newsletter: Newsletter,
},
});
If you forget any of these three, defineTheme() throws on startup with a clear error pointing at the missing piece.
What sections you don’t have to implement
A theme must declare a renderer for every type in SECTION_REGISTRY that’s actually used by a page on a site that opts into this theme. Sites can be configured to disable specific section types, in which case your theme doesn’t need a renderer for them.
The publish-time scan checks coverage: it walks every page on every site this theme is applied to and confirms every section type that page contains has a renderer in src/theme.ts. A missing renderer fails the publish with a list of which (theme, page, type) tuples are broken.
Reference: every shipped section type
The authoritative list lives in SECTION_REGISTRY — your IDE’s Go-To-Definition on that import opens the schema package. Major categories:
- Content blocks:
hero,text_with_image,rich_text,image,testimonial,faq,cta - Lists + grids:
grid,collection_grid,product_grid - Commerce:
product_card,product_detail,category_breadcrumb - Forms:
newsletter,contact_form - Blog:
blog_list,blog_post
Each ships a Zod schema with field-level validation, defaults, and JSDoc that the editor surfaces as help text.