If your Sanity page builder started as six tidy blocks and is now a 40-item dropdown that editors are scared to open, you are in a common place. Page builders rot the same way every time: one-off requests become new block types, nobody removes the old ones, and the frontend renderer turns into a giant switch statement. This post covers how to recognise it, what it costs to leave it, and the order I would fix it in.
Symptoms of a page builder that has gotten away from you
You rarely hear "the schema is bad". You hear things like these:
- Editors ask which of three "Hero" blocks to use, and the answer is "it depends who built the page".
- Two blocks look nearly identical but have different field names, so content cannot be copied between them.
- Pages break visually when someone fills in an optional field nobody tested.
- The Studio page document takes several seconds to open, and the pageBuilder array feels laggy once a page has 15 or more blocks.
- Developers are reluctant to touch the renderer because removing a case might break a page nobody remembers.
- Marketing keeps asking for "a new block" because the existing ones are either too rigid or too loose.
The last two are the real signal. When developers fear deleting and editors keep asking for additions, the model is only growing, never being corrected.
What it costs to ignore
None of this shows up as an outage, which is why it lingers. The costs are slow and spread out:
- Editor time. Every page takes longer to build, and layouts drift page to page. Brand consistency is an editor discipline problem when it should be a schema decision.
- Developer time. Each new campaign page needs someone to check which blocks render correctly. Small changes become half-day jobs.
- Performance. Every block type your renderer can output usually ends up in the bundle, even if only five are used on live pages. See my post on cutting Next.js bundle size for how to measure that.
- Query weight. Projections for 40 block types get long, and pages fetch fields they never render.
- Studio speed. The slower the editor, the more people go back to Google Docs and ask a developer to paste content in. I cover that dynamic in why editors avoid the Studio.
Step 1: find out what is actually used
Before designing anything, measure. A block that exists in the schema but appears on zero published pages is free to remove. A block used on 200 pages is expensive to change. You can answer this with GROQ in the Vision tool, one block type at a time:
// Pages still using a specific legacy block
*[_type == "page" && count(pageBuilder[_type == "heroOld"]) > 0]{
title,
"slug": slug.current,
"uses": count(pageBuilder[_type == "heroOld"])
}
For a full picture, fetch every page's block types in a script and tally them in TypeScript. Remember to include drafts if editors have unpublished work in progress, otherwise you will delete something someone is using. Put the result in a spreadsheet with three columns: block type, page count, and a verdict (keep, merge, remove).
Step 2: merge near-duplicates, do not just rename them
The biggest win is usually collapsing blocks that differ by layout rather than purpose. Three hero blocks (hero with image, hero with video, centered hero) are often one hero with a variant field and a media field that accepts either an image or a video.
The rule I use: a block should exist because the content shape is different, not because the styling is different. Styling choices belong in a small, constrained set of options such as a variant list with three or four values, not in free-form toggles. Too many toggles recreate the original problem inside a single block.
Step 3: make the frontend tell you what is registered
A switch statement hides what is supported. A typed registry makes removals visible, because a missing key becomes a compile error. This is the pattern I put in place before deleting anything:
// components/page-builder/registry.tsx
import type { ComponentType } from 'react'
import { Hero } from './Hero'
import { FeatureGrid } from './FeatureGrid'
import { CtaBanner } from './CtaBanner'
// Block types the frontend can render. Keep in sync with the schema.
export const blockRegistry = {
hero: Hero,
featureGrid: FeatureGrid,
ctaBanner: CtaBanner,
} satisfies Record<string, ComponentType<any>>
export type BlockType = keyof typeof blockRegistry
export function isKnownBlock(type: string): type is BlockType {
return type in blockRegistry
}
The renderer then loops over the array, skips unknown types (and logs them in development), and looks components up by key. Combined with Sanity TypeGen, a block removed from the schema shows up as a type error instead of a blank section on a live page. If heavy blocks such as video or carousels are in the registry, load them with dynamic imports so they only ship where they are used.
Step 4: migrate content, then delete the schema
The order matters, and it is the same as any live-site model change:
- Add the new merged block to the schema and the renderer.
- Write a migration script that converts old block objects into the new shape, run against a copy of the dataset first.
- Spot-check pages visually, including ones with unusual content.
- Deploy the frontend that no longer renders the old type.
- Only then remove the old type from the schema.
If you delete the schema type first, existing documents show up in the Studio as invalid or unknown items, and editors cannot easily fix them. I go into the safe sequence in restructuring a Sanity schema on a live site, and pruning leftover types is covered in the Studio bloat audit.
Step 5: improve the editing experience while you are in there
Once the model is smaller, a few cheap changes keep it from regrowing:
- Give every block a preview with a title and thumbnail-style subtitle, so a 15-block page is scannable.
- Group the block picker into categories, and limit which blocks are allowed on which page types.
- Add validation on fields that break layouts, such as headline length or missing alt text.
- Write a one-page rule for adding a block: who approves it, what it replaces, and how it gets removed later.
The last point is the real fix. Page builders rot because adding is easy and removing is nobody's job.
What to ask a developer you hire for this
- How will you find out which blocks are used, including drafts?
- Will you migrate content with a script I can see, run on a dataset copy first?
- What happens to pages that use a block you are removing?
- Will the frontend fail loudly on an unknown block, or render nothing?
- Can editors keep working during the migration?
Vague answers to the first and third questions are the red flag.
When this is worth paying for
If pages take editors a long time to assemble, or developers avoid touching the renderer, a page builder cleanup usually pays back quickly in fewer support requests. This is work I do. A Performance & Sanity audit, from $800, is the usual starting point: I map block usage, flag what to merge, and give you a fixed scope before any migration begins.
Top comments (0)