# Inside the Turbo Start Sanity page-builder pattern

The page-builder is the heart of Turbo Start Sanity. Here is how a block flows from schema to GROQ to a typed React component, and how to add your own.

![Neon yellow and orange light streaks crossing a dark dotted grid](https://cdn.sanity.io/images/s6kuy1ts/production/57efbed4ccc2c41cd2bb2821848afe69b12edd57-4096x2596.png?w=1600&fm=webp&q=80&auto=format)

The core content model in Turbo Start Sanity is a page-builder: an array of typed blocks that editors arrange to compose a page. What makes it maintainable is not the idea — every CMS has one — but how strictly each block is kept self-contained across the schema, the query and the rendering layers.

## One block, one folder

Each block lives in its own directory under the blocks package with three files: a `.schema.ts` that defines the Sanity fields, a `.groq.ts` that projects exactly the data the component needs, and a headless `index.tsx` component. Keeping the projection next to the component that consumes it means the query and the UI cannot quietly drift apart — change one, you are staring at the other.

All block schemas are re-exported as `blockSchemas`. The Studio merges them into its schema types and maps them into the page-builder array definition, so registering a block in one place lights it up in the editor.

## Rendering is a typed switch

On the frontend, a single dispatcher maps each block's `_type` to its component. The cast to a per-block type is the safety net: if a GROQ projection or a schema field is renamed, the assertion fails to compile instead of passing `any` straight through to render.

```tsx
function renderBlockComponent(block) {
  switch (block?._type) {
    case "cta":
      return <CTABlock {...(block as PagebuilderType<"cta">)} />;
    case "hero":
      return <HeroBlock {...(block as PagebuilderType<"hero">)} />;
    case "faqAccordion":
      return <FaqAccordion {...(block as PagebuilderType<"faqAccordion">)} />;
    // ...one case per block
    default:
      return null;
  }
}
```

A block that returns `null` — an unknown type — renders a visible "component not found" placeholder rather than failing silently, so a mismatch is obvious in preview. The whole builder is wrapped in optimistic updates and `createDataAttribute` calls, which is what makes click-to-edit work in the Presentation tool.

## Adding a new block, end to end

The pattern is deliberately mechanical, which is what makes it fast and AI-friendly:

1. Create the block folder with its `.schema.ts`, `.groq.ts` and `index.tsx`.
2. Export it and add it to `blockSchemas` so the Studio picks it up.
3. Regenerate Sanity types so the frontend sees the new shape.
4. Add its GROQ fragment and include it in the page-builder projection.
5. Build the styled component in the web app and register it in `renderBlockComponent`.
6. Add a Markdown serializer case so the block degrades to semantic Markdown in `.md` output — without it, the block renders blank for agents.

## Why the discipline pays off

Because every layer is colocated and typed, the failure modes are loud and early: a rename breaks the build, a missing serializer shows up in Markdown output, an unregistered block shows a placeholder in preview. You trade a little upfront ceremony for a page-builder where adding the tenth block is exactly as boring as adding the second. Boring, here, is the whole point.
