Skip to main content
The block registry is a single TypeScript file that auto-discovers every block component and exposes them as a unified lookup map. It is the mechanism that makes the page builder work without any manual registration.

Full source

// astro-app/src/components/block-registry.ts

// All blocks — auto-discovered via import.meta.glob
// Custom blocks: PascalCase filename → camelCase _type (e.g. HeroBanner.astro → heroBanner)
// UI blocks: kebab-case filename used directly as _type (e.g. hero-1.astro → hero-1)
import type { AstroComponentFactory } from 'astro/runtime/server/render/astro/factory';

const allBlocks: Record<string, AstroComponentFactory> = {};

const customModules = import.meta.glob('./blocks/custom/*.astro', { eager: true });
for (const [path, mod] of Object.entries(customModules)) {
  const filename = path.split('/').pop()!.replace('.astro', '');
  const typeName = filename[0].toLowerCase() + filename.slice(1);
  allBlocks[typeName] = (mod as { default: AstroComponentFactory }).default;
}

const uiModules = import.meta.glob('./blocks/*.astro', { eager: true });
for (const [path, mod] of Object.entries(uiModules)) {
  const name = path.split('/').pop()!.replace('.astro', '');
  allBlocks[name] = (mod as { default: AstroComponentFactory }).default;
}

export { allBlocks };

How custom blocks are mapped

The custom block loop reads each *.astro file from ./blocks/custom/, strips the extension, and lowercases the first character:
const filename = path.split('/').pop()!.replace('.astro', '');
// 'HeroBanner'

const typeName = filename[0].toLowerCase() + filename.slice(1);
// 'heroBanner'

allBlocks[typeName] = (mod as { default: AstroComponentFactory }).default;
The resulting key must match the name field in the Sanity schema for that block. Because defineBlock creates a Sanity object type with the name you provide, keeping the PascalCase filename and the camelCase schema name in sync is the only naming convention you need to follow.

Current custom block registrations

FilenameRegistered _type
AnnouncementBar.astroannouncementBar
ArticleList.astroarticleList
ComparisonTable.astrocomparisonTable
ContactForm.astrocontactForm
CtaBanner.astroctaBanner
Divider.astrodivider
EventList.astroeventList
FaqSection.astrofaqSection
FeatureGrid.astrofeatureGrid
HeroBanner.astroheroBanner
ImageGallery.astroimageGallery
LogoCloud.astrologoCloud
ProjectCards.astroprojectCards
Pullquote.astropullquote
RichText.astrorichText
SponsorCards.astrosponsorCards
SponsorshipTiers.astrosponsorshipTiers
SponsorSteps.astrosponsorSteps
StatsRow.astrostatsRow
TeamGrid.astroteamGrid
Testimonials.astrotestimonials
TextWithImage.astrotextWithImage
Timeline.astrotimeline
VideoEmbed.astrovideoEmbed

How template blocks are mapped

The template block loop reads each *.astro file from ./blocks/ (the root of the blocks directory, not custom/) and uses the filename verbatim as the key:
const name = path.split('/').pop()!.replace('.astro', '');
// 'hero-1'

allBlocks[name] = (mod as { default: AstroComponentFactory }).default;
No transformation — the kebab-case filename becomes the _type string that must appear in Sanity.

The allBlocks export

allBlocks is a plain Record<string, AstroComponentFactory>. An AstroComponentFactory is the internal Astro type for a renderable component — exactly what you get from a .astro file’s default export. Only BlockRenderer imports allBlocks. Nothing else in the codebase references it directly.

How BlockRenderer uses allBlocks

{blocks.map((block) => {
  const Component = allBlocks[block._type];
  if (!Component) return null;
  // ...
  return (
    <BlockWrapper ...>
      <Component {...block} {...extraProps} />
    </BlockWrapper>
  );
})}
  1. block._type is the Sanity type name stored on each block in the blocks[] array.
  2. allBlocks[block._type] either resolves to a component or is undefined.
  3. If undefined, the block is skipped — the page renders without it and no error is thrown.
  4. If resolved, the component receives every field on the block object as a top-level prop via {...block}.
If a block appears in Sanity but nothing renders on the page, the most common cause is a mismatch between the Sanity schema name and the filename-derived _type key. Check allBlocks in the browser’s Vite module graph or add a console.log(Object.keys(allBlocks)) in BlockRenderer to debug.

Why there is no switch statement

Earlier versions of similar projects use a switch (block._type) in the renderer with an explicit case for every block type. This creates a maintenance burden: every new block requires editing the renderer. The registry pattern eliminates that. Adding a block means:
  1. Drop an .astro file in the right directory.
  2. The import.meta.glob call picks it up at build time.
  3. allBlocks gains the new key automatically.
No renderer edits. No registration file updates. No import lists.

Build docs developers (and LLMs) love