Skip to content

BlockRenderer

The BlockRenderer is the core component that turns CMS block data into rendered Astro components. It takes an array of blocks, looks up the matching component for each block type, and renders them in order.

---
import BlockRenderer from '@wollycms/astro/components/BlockRenderer.astro';
import * as blocks from '../lib/blocks';
const page = await wolly.pages.getBySlug('home');
---
<BlockRenderer
blocks={page.regions.hero ?? []}
region="hero"
components={blocks}
/>
<BlockRenderer
blocks={page.regions.content ?? []}
region="content"
components={blocks}
/>
PropTypeDescription
blocksResolvedBlock[]Array of blocks to render (from page.regions.*)
regionstringName of the region being rendered
componentsRecord<string, any>Object mapping block type slugs to Astro components

Create src/lib/blocks.ts to map each block type slug to its component:

export { default as hero } from '../blocks/Hero.astro';
export { default as rich_text } from '../blocks/RichText.astro';
export { default as image } from '../blocks/ImageBlock.astro';
export { default as cta } from '../blocks/CTA.astro';
export { default as testimonial } from '../blocks/Testimonial.astro';
export { default as code_block } from '../blocks/CodeBlock.astro';

The export name must match the block type slug. When BlockRenderer encounters a block with block_type: "hero", it looks for components.hero in the mapping.

Each block component receives these props:

interface BlockProps {
fields: Record<string, unknown>;
block: {
id: string;
type: string;
title?: string;
is_shared?: boolean;
};
region: string;
position: number;
}
src/blocks/Hero.astro
---
const { fields, block, region, position } = Astro.props;
const heading = fields.heading as string;
const description = fields.description as string | undefined;
const ctaText = fields.cta_text as string | undefined;
const ctaUrl = fields.cta_url as string | undefined;
---
<section class="hero">
<h1>{heading}</h1>
{description && <p>{description}</p>}
{ctaText && ctaUrl && (
<a href={ctaUrl} class="btn">{ctaText}</a>
)}
</section>
src/blocks/RichTextBlock.astro
---
import RichText from '@wollycms/astro/components/RichText.astro';
const { fields } = Astro.props;
---
<div class="prose">
<RichText content={fields.body} />
</div>

The RichText component converts TipTap JSON to HTML, handling paragraphs, headings, lists, links, images, code blocks, tables, and text marks (bold, italic, underline, strikethrough, code).

If BlockRenderer encounters a block type that has no matching component in the mapping, it renders a fallback:

<div class="wolly-block-unknown" data-type="missing_type">
Unknown block type: missing_type
</div>

This makes it easy to spot unmapped block types during development.

A typical page layout renders several regions:

---
import BlockRenderer from '@wollycms/astro/components/BlockRenderer.astro';
import * as blocks from '../lib/blocks';
const page = await wolly.pages.getBySlug(Astro.params.slug || 'home');
---
<header>
<BlockRenderer blocks={page.regions.hero ?? []} region="hero" components={blocks} />
</header>
<main>
<BlockRenderer blocks={page.regions.content ?? []} region="content" components={blocks} />
</main>
<aside>
<BlockRenderer blocks={page.regions.sidebar ?? []} region="sidebar" components={blocks} />
</aside>
<footer>
<BlockRenderer blocks={page.regions.footer ?? []} region="footer" components={blocks} />
</footer>

Block components receive region and position, which are useful for conditional styling:

---
const { fields, region, position } = Astro.props;
const isFirst = position === 0;
---
<section class:list={[
'cta',
{ 'cta--sidebar': region === 'sidebar' },
{ 'cta--hero': region === 'hero' },
{ 'mt-0': isFirst },
]}>
<h2>{fields.heading}</h2>
</section>