Rendering Storyblok Rich Text in Astro

Edvinas Jurelė, Senior Front-end Engineer @NordVPN

July 13, 2023


0

Rendering Rich Text elements in headless content management systems (CMS) like Storyblok can be challenging. We decided to carry out this process with Astro. Here is what we learned in the process.

Blog inside image storyblok rich text rendering in astro

Using an official integration

The Storyblok CMS has many integrations for various frontend frameworks, including React, Vue, and Svelte. Luckily for us, they also integrated it for Astro. Astro is the new kid on the block, with a vibrant and active community and a responsive developer team. These are just a few of the reasons that we chose Astro for website development.

Headless CMS platforms are designed to provide content through APIs, allowing developers to use that content in various applications and front-end frameworks. The most straightforward way of using Storyblok in the Astro framework is through its official storyblok-astro integration.

Rich Text elements often contain HTML tags, inline styles, and other formatting options that need to be properly rendered on the front end. Besides common WYSIWYG (“what you see is what you get”) editor capabilities, Storyblok is capable of allowing content editors to embed such elements as inline blocks (components), custom styling, emojis, quotes, and code snippets.

The official integration provides an easy way to render Rich Text by using the renderRichText function that comes with @storyblok/astro:

1
import { RichTextSchema, renderRichText } from "@storyblok/astro";
2
import cloneDeep from "clone-deep";
3
4
const mySchema = cloneDeep(RichTextSchema);
5
6
const { blok } = Astro.props;
7
8
const renderedRichText = renderRichText(blok.text, {
9
schema: mySchema,
10
resolver: (component, blok) => {
11
switch (component) {
12
case "my-custom-component":
13
return `<div class="my-component-class">${blok.text}</div>`;
14
break;
15
default:
16
return `Component ${component} not found`;
17
}
18
},
19
});

A challenge

Although the renderRichText from @storyblok/astro works fine and covered our most basic needs, it quickly turned out to be limiting and problematic for the following reasons:

  1. The renderRichText utility cannot map Rich Text elements to actual Astro components and so cannot render embedded Storyblok components inside the Rich Text field in CMS.

  2. Links that you might want to pass through your app's router cannot be reused because they require the actual function to be mapped with data.

    It is hard to maintain the string values, especially when complex needs arise — for example, when setting classes and other HTML properties dynamically. It may be possible to minimize the complexity by using some HTML parsers like ultrahtml, but that does not eliminate the problem entirely.

The solution

Instead of dealing with HTML markup, storyblok-rich-text-astro-renderer provides a capability to convert any Storyblok CMS Rich Text data structure into the nested component nodes structure — { component, props, content } — and render it with Astro. The configuration is easily extended to meet all project needs.

The package delivers:

  • The RichTextRenderer.astro helper component, which provides options to map any Storyblok Rich Text element to any custom component (for example, Astro, SolidJS, Svelte, or Vue).

  • The resolveRichTextToNodes resolver utility can potentially reuse the transform utility before rendering the structure manually.

Using the package

The usage of storyblok-rich-text-astro-renderer is simple, yet flexible:

1
---
2
import RichTextRenderer, { type RichTextType } from "storyblok-rich-text-astro-renderer/RichTextRenderer.astro";
3
import { storyblokEditable } from "@storyblok/astro";
4
5
export interface Props {
6
blok: {
7
text: RichTextType;
8
};
9
}
10
11
const { blok } = Astro.props;
12
const { text } = blok;
13
---
14
15
<RichTextRenderer content={text} {...storyblokEditable(blok)} />

Sensible default resolvers for marks and nodes are provided out of the box. You only have to provide resolvers if you want to override the default behavior.

Use resolver to enable and control the rendering of embedded components, and schema to control how you want the nodes and marks to be rendered:

1
<RichTextRenderer
2
content={text}
3
schema={{
4
nodes: {
5
heading: ({ attrs: { level } }) => ({
6
component: Text,
7
props: { variant: `h${level}` },
8
}),
9
paragraph: () => ({
10
component: Text,
11
props: {
12
class: "this-is-paragraph",
13
},
14
}),
15
},
16
marks: {
17
link: ({ attrs }) => {
18
const { custom, ...restAttrs } = attrs;
19
20
return {
21
component: Link,
22
props: {
23
link: { ...custom, ...restAttrs },
24
class: "i-am-link",
25
},
26
};
27
},
28
}
29
}}
30
resolver={(blok) => {
31
return {
32
component: StoryblokComponent,
33
props: { blok },
34
};
35
}}
36
{...storyblokEditable(blok)}
37
/>

Conclusion

That’s all there is to it! We just made rendering Storyblok Rich Text in Astro much easier.

The storyblok-rich-text-astro-renderer package offers customization options and improves frontend development workflow, enabling you to tailor the rendering behavior to your project's specific requirements.

Even though Astro supports numerous integrations of React, Svelte, and Vue, when you want to go with bare Astro, the storyblok-rich-text-astro-renderer package is the right choice.