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.

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.

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.