Rendering Elements from a Headless CMS
In my previous blog post, I shared my journey of migrating my website from a React Single-Page Application (SPA) to Astro. One of the main motivations for making this switch was to take advantage of Astro's speed, simplicity, and flexibility. Now that I've settled into using Astro, I wanted to tackle the next step: creating a blog page for my site.
When I started researching how to build a blog in Astro, I stumbled upon the concept of headless CMS. A headless CMS allows you to manage content (like blog posts) separately from your frontend, giving you more control over how your content is delivered and displayed. But with so many headless CMS providers out there—like Contentful, Ghost, and Strapi—it was overwhelming to choose just one.
So, how did I land on Storyblok? Well, I wanted something simple, intuitive, and—most importantly—free. Storyblok offers a generous free plan that includes all the essential features I needed to get started. Plus, its real-time preview and visual editor made it feel more user-friendly compared to some of the other options.
For a detailed guide on setting up Storyblok with Astro, I recommend checking out the official Astro documentation and Storyblok's technology hub for Astro . These resources provide a comprehensive walkthrough of the integration process, so I won't repeat those steps here.
A Better Rich Text Renderer
The official Storyblok + Astro integration provides a basic way to render rich text using a universal utility from @storyblok/js. However, this approach has several limitations:
- It cannot map rich text elements to Astro components.
- It doesn't handle links that need to integrate with your app's router.
- Managing dynamic HTML properties is challenging and may require additional parsers.
The solution is to use storyblok-rich-text-astro-renderer. For instance, here's how I used Astro's <Code /> component to customize how code blocks are visually presented:
import { storyblokEditable } from '@storyblok/astro'
import StoryblokComponent from '@storyblok/astro/StoryblokComponent.astro'
import type { RichTextType } from 'storyblok-rich-text-astro-renderer'
import RichTextRenderer from 'storyblok-rich-text-astro-renderer/RichTextRenderer.astro'
import { Code } from 'astro:components'
export interface Props {
blok: {
text: RichTextType
}
}
const { blok } = Astro.props
const { text } = blok
---
<RichTextRenderer
content={text}
schema={{
nodes: {
code_block: ({ attrs, content }) => ({
component: Code,
props: {
lang: attrs.class?.split('-')[1],
code: content?.[0].text,
class: 'not-prose font-mono p-4 leading-tight',
theme: 'catppuccin-macchiato',
},
}),
},
}}
resolver={(blok) => {
return {
component: StoryblokComponent,
props: { blok },
}
}}
{...storyblokEditable(blok)}
/>