Before you start, make sure you are on a feature branch created from
preview: git checkout preview && git pull && git checkout -b feat/block-your-block-name.Overview
Adding a custom block touches five areas:- Sanity schema — define the block’s fields
- Schema registration — add the block to the studio
- fulldev/ui primitives — install any UI components the block needs
- Astro component — implement the block using the flat-props pattern
- GROQ projection and typegen — expose the data and generate types
Steps
Create the Sanity schema
Create a new file in The
studio/src/schemaTypes/blocks/. Use the defineBlock helper, which automatically adds the shared base fields (backgroundVariant, spacing, maxWidth) to every block and generates a collapsible Layout Options fieldset.defineBlock helper also supports variants (for layout variant radio buttons), hiddenByVariant (to hide fields based on the selected variant), and an icon for the Sanity Studio block picker.For reference, here is how heroBanner uses variants and hiddenByVariant:Register the schema
Open 2. Add it to the Then open the page schema at
studio/src/schemaTypes/index.ts and add two things:1. Import the schema at the top:schemaTypes array:studio/src/schemaTypes/documents/page.ts and add yourBlock to the blocks[] array’s of list so editors can select it:Install fulldev/ui primitives
If your block needs UI components (buttons, badges, icons, images), install them from the fulldev registry via the shadcn CLI. Do not copy components manually.Installed components land in
astro-app/src/components/ui/. Import them from @/components/ui/{name}.Skip this step if your block only uses components that are already installed.Create the Astro component
Create Key conventions:
astro-app/src/components/blocks/custom/YourBlock.astro. The filename must be PascalCase and the first character lowercased must match the schema name exactly (YourBlock → yourBlock).All blocks use the flat-props pattern: interface Props extends YourBlockBlock and destructure fields directly from Astro.props.- Use
stegaClean(value)on any string field used for logic (variant selection, conditional rendering). Raw strings from Sanity may contain stega metadata in the preview branch. - Compose from components in
src/components/ui/— do not write raw HTML for things buttons or badges cover. - Add
data-animateto the outer<Section>to opt into the intersection-observer entrance animation. - Interactivity goes in a
<script>tag using vanilla JS with data-attribute event delegation. Keep each handler under 50 lines.
block-registry.ts uses import.meta.glob, the file is automatically registered as soon as it exists. No other file needs to be updated.Add the GROQ projection and run typegen
Open After adding the projection, regenerate TypeScript types:This creates the
astro-app/src/lib/sanity.ts (or the relevant query file) and add a projection for your block type inside the blocks[] projection. The projection reshapes Sanity’s data into the flat structure your component expects.YourBlockBlock type that your component imports from @/lib/types.Finally, build and verify Lighthouse scores hold at 90+:Checklist
Before opening a PR, confirm:- Schema file created in
studio/src/schemaTypes/blocks/ - Schema imported and added to
schemaTypesarray inindex.ts - Block type added to the page schema’s
blocks[]array - Astro component created in
blocks/custom/with PascalCase filename - Component uses
interface Props extends YourBlockBlock(flat-props pattern) -
stegaCleanused on any string used for conditional logic - GROQ projection added and
npm run typegenrun successfully - Block renders correctly in local dev with
npm run dev - Lighthouse scores remain at 95+ Performance, 90+ Accessibility