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:
- Create the block folder with its
.schema.ts,.groq.tsandindex.tsx. - Export it and add it to
blockSchemasso the Studio picks it up. - Regenerate Sanity types so the frontend sees the new shape.
- Add its GROQ fragment and include it in the page-builder projection.
- Build the styled component in the web app and register it in
renderBlockComponent. - Add a Markdown serializer case so the block degrades to semantic Markdown in
.mdoutput — 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.