# Build your first Sanity page-builder block, step by step

A hands-on walkthrough: go from an empty folder to a live, typed, editor-ready page-builder block in Turbo Start Sanity without missing a single wire.

![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)

Adding a block to the Turbo Start Sanity page-builder is one of those tasks that feels fiddly until you have done it once. There are seven touch points, and skipping any one leaves the block half-wired. This walkthrough builds a simple "callout" block end to end so you can see every step in context.

## What we are building

A callout: a short heading, a body paragraph and a tone (info or warning). Nothing fancy — the point is the wiring, not the block.

## 1. Create the block folder

Every block lives in its own directory under the blocks package with three files. Start with the schema:

```ts
import { defineField, defineType } from "sanity";

export const callout = defineType({
  name: "callout",
  type: "object",
  fields: [
    defineField({ name: "heading", type: "string" }),
    defineField({ name: "body", type: "text" }),
    defineField({
      name: "tone",
      type: "string",
      options: { list: ["info", "warning"], layout: "radio" },
      initialValue: "info",
    }),
  ],
});
```

## 2. Register it so the Studio sees it

Export the schema from the package entry and add it to `blockSchemas`. The Studio merges that array into its schema types and into the page-builder array definition, so the block appears in the "add block" menu with no further wiring on the Studio side.

## 3. Regenerate types

Schema changed, so the generated types are now stale. Run the extract pass, then the type pass:

```bash
pnpm extract   # snapshot the new schema
pnpm type      # regenerate query result types
```

Do them in that order. Generating types against a schema you never re-extracted is the classic way to spend an hour debugging a type that was correct all along.

## 4. Add a GROQ projection

Co-locate the projection with the block, and list the fields explicitly rather than spreading everything:

```ts
export const calloutGroqProjection = /* groq */ `
  _type == "callout" => { _type, _key, heading, body, tone }
`;
```

Then include it in the shared `pageBuilderFragment` so the block's data is actually fetched with the rest of the page.

## 5. Build the component

```tsx
export function Callout({ heading, body, tone }) {
  return (
    <aside data-tone={tone}>
      <strong>{heading}</strong>
      <p>{body}</p>
    </aside>
  );
}
```

## 6. Register it in the renderer

Add a case to `renderBlockComponent` in the web app's page-builder, casting to the generated type so a future rename breaks the build instead of shipping `any`:

```tsx
case "callout":
  return <Callout {...(block as PagebuilderType<"callout">)} />;
```

An unregistered block renders a visible "component not found" placeholder in preview, so if you forget this step you will notice immediately.

## 7. Add a Markdown serializer

The last step is the one people miss. Turbo Start Sanity serves every page as Markdown for agents, and each block needs a serializer or it renders blank in `.md` output:

```ts
export function calloutToMarkdown(block) {
  return `> **${block.heading}**\n>\n> ${block.body}`;
}
```

Wire it into the block-to-Markdown dispatcher, and add a test asserting no JSX leaks into the output.

## The payoff

Seven steps, but every one has a loud failure mode: a rename breaks the build, a missing serializer shows up in Markdown, an unregistered block shows a placeholder. Once you have felt the rhythm once, the tenth block takes ten minutes — and an AI assistant can follow the exact same checklist to scaffold the next one for you.
