Page builderHumanist Studio
The block that editors could insert and nobody could see
A Sanity page builder has two registries — one for the Studio, one for React — and nothing makes them agree. Here is what that cost, and the forty-line script that stops it happening again.
A page builder in Next.js and Sanity is two lists that have to match. One is the schema array the Studio reads, which decides what an editor can insert. The other is the component map React reads, which decides what actually renders. They live in different files, they are edited at different times, and the framework does not know they are related.
For several months this repository had two blocks in the first list and not the second. An editor could open a page, insert a TextCenter block, fill it in, publish, and see a gap where their content should be. No error. Not in production, not in development, not in the build. The block was simply absent from the map, so PageBuilder looked up its type, found nothing, and returned null.
Why nothing complained
The rendering loop was written the way most of them are:
const componentMap = {
Accordion: dynamic(() => import("@/components/Accordion/Accordion")),
Video: dynamic(() => import("@/components/Video/Video")),
// TextCenter and Form were registered in Sanity. Not here.
};
export default function PageBuilder({ data }) {
return data.map((block) => {
const Component = componentMap[block._type];
return Component ? <Component key={block._key} data={block} /> : null;
});
}That null is the whole problem. It is the correct thing to return for a block you do not recognise — you do not want a page to crash because someone added a new type — and it is also a perfect way to hide a mistake. Silence is a feature until it is the bug.
The first fix was the obvious one: warn in development. PageBuilder now logs the unmapped type and the file to add it to. That helps the person who is looking. It does nothing for the person who is not.
The fix that does not depend on anyone looking
What actually closed the gap is a script that runs before every build and asserts the two lists agree, in both directions:
for (const name of schemas) {
if (!fs.existsSync(`components/${name}/${name}.jsx`))
problems.push(`${name}: no component`);
if (!new RegExp(`^\\s{2}${name}:`, "m").test(registry))
problems.push(`${name}: missing from componentMap`);
if (!new RegExp(`\\b${name}\\b`).test(groups))
problems.push(`${name}: not in any group`);
}It reads the schema directory for block names, then checks each one has a component file, a line in the component map, and a place in a group. Then it checks the reverse — a component with no schema is dead weight, and dead weight is where the next inconsistency hides. Any mismatch exits non-zero, and npm run build stops.
It is wired into prebuild, so it is not a step anyone has to remember. Forty lines, no dependencies, and it found something the first time it ran: a component sitting in the blocks directory that was not a block at all.
The rule underneath
A registry that must be kept in sync by hand will drift. The question is only whether you find out from a script or from an editor.
The same shape of problem shows up wherever two files describe one thing — a schema and its TypeScript type, a route and its sitemap entry, a component and its documentation. The answer is the same each time: do not trust the discipline, write the check. It is always shorter than the incident report.