Skip to content

@storyblok/richtext is a custom renderer for Storyblok rich text content in JavaScript applications.

Add the package to a project by running this command in the terminal:

Terminal window
npm install @storyblok/richtext@latest
import { renderRichText } from "@storyblok/richtext";
const html = renderRichText(document);
document.querySelector("#app").innerHTML = `<div>${html}</div>`;

The following nodes and marks are supported by the rich text renderer and parser APIs.

  • doc
  • paragraph
  • heading
  • blockquote
  • ordered_list
  • bullet_list
  • list_item
  • horizontal_rule
  • hard_break
  • code_block
  • image
  • emoji
  • table
  • tableRow
  • tableCell
  • tableHeader
  • blok
  • bold
  • italic
  • underline
  • strike
  • code
  • link
  • superscript
  • subscript
  • highlight
  • textStyle
  • anchor
  • styled

Rendering can be customized by providing renderer functions for individual nodes and marks. When using TypeScript, all supported nodes and marks are fully type-safe.

Create a custom renderer as demonstrated in the example below:

import { renderRichText } from "@storyblok/richtext";
const options = {
renderers: {
paragraph: ({ content, context }) => `<p class="my-paragraph">${renderRichText(content, context)}</p>`,
heading: ({ children, attrs }) => {
const level = attrs.level;
return `<h${level} class="my-heading">${children}</h${level}>`;
},
bold: ({ children }) => `<strong class="my-bold">${children}</strong>`,
},
};
const html = renderRichText(document, options);

The following properties are available to renderer functions.

Node renderers

  • attrs
  • children
  • content
  • marks
  • context

Mark renderers

  • attrs
  • children

For nodes, such as paragraphs or headings, either the rendered children value can be used directly or the raw content can be accessed. The context object provides access to the options, allowing child nodes to be rendered recursively when required.

The example above demonstrates both approaches.

Storyblok components are embedded in rich text as blok nodes. Each blok node contains an attrs.body array with one or more Storyblok components that must be rendered manually.

import type { SbRichTextProps, renderRichText } from "@storyblok/richtext";
const MyBlokRenderer = ({ attrs }: SbRichTextProps<"blok">) => {
const body = Array.isArray(attrs?.body) ? attrs.body : [];
return body.map((blok) => `<button data-uid="${blok._uid}">${blok.title}</button>`).join("");
};
const html = renderRichText(document, {
renderers: {
blok: MyBlokRenderer,
},
});

When creating custom table renderers, the splitTableRows helper can be used to separate header rows from body rows.

import { renderRichText, splitTableRows, type SbRichTextRenderContext } from "@storyblok/richtext";
const options: SbRichTextRenderContext = {
renderers: {
table: ({ content, context }) => {
const { headerRows, bodyRows } = splitTableRows(content);
return `
<table class="custom-table">
${headerRows ? `<thead>${renderRichText(headerRows, context)}</thead>` : ""}
<tbody>${renderRichText(bodyRows, context)}</tbody>
</table>
`;
},
},
};
const html = renderRichText(document, options);

The splitTableRows helper separates table rows into two collections:

  • headerRows contains rows with header cells (table_header).
  • bodyRows contains all remaining rows.

This helper is framework-agnostic and can be used in any custom renderer implementation, including framework SDK integrations such as @storyblok/vue, @storyblok/react, @storyblok/astro, and @storyblok/svelte.

To optimize images, use the optimizeImage property on the renderRichText options. For a full list of available options, refer to the Image Service documentation.

import { renderRichText } from "@storyblok/richtext";
const html = renderRichText(document, {
optimizeImage: {
class: "my-peformant-image",
loading: "lazy",
width: 800,
height: 600,
srcset: [400, 800, 1200, 1600],
sizes: ["(max-width: 400px) 100vw", "50vw"],
filters: {
format: "webp",
quality: 10,
grayscale: true,
blur: 10,
brightness: 10,
},
},
});

The package exports a set of TypeScript types that can be used when building custom renderers integrations.

import type {
SbRichTextDoc,
SbRichTextNode,
SbRichTextMark,
SbRichTextRenderContext,
SbRichTextRendererMap,
SbRichTextProps,
SbRichTextImageOptions,
} from "@storyblok/richtext";
TypeDescription
SbRichTextDocThe root rich text document type.
SbRichTextNodeUnion type representing all supported rich text nodes.
SbRichTextMarkUnion type representing all supported rich text marks.
SbRichTextRenderContextRendering context passed to node renderers.
SbRichTextRendererMapType used when defining custom renderer mappings.
SbRichTextPropsRenderer props for all supported nodes and marks.
SbRichTextImageOptionsConfiguration options for image optimization.

The package includes a utility for converting HTML content to Storyblok’s rich text format.

import { htmlToStoryblokRichtext } from "@storyblok/richtext/html-parser";
const html = `
<h1>Main Heading</h1>
<p>This is a <strong>bold</strong> paragraph with <em>italic</em> text.</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
</ul>
<blockquote>
<p>This is a blockquote</p>
</blockquote>
`;
const richtextDoc = htmlToStoryblokRichtext(html);
const html = renderRichText(richtextDoc);
document.getElementById("content").innerHTML = html;

Parsing behavior can be customized by providing custom parsers.

const html = `<div class="content">
<p>
This is a simple paragraph with
<strong>bold text</strong>,
<em>italic text</em>, and a
<a href="https://example.com">link to Example</a>.
</p>
<p>
You can also place a link in between a sentence, like
<a href="https://astro.build">Astro</a>,
which is a great framework for building fast websites.
</p>
<div class="card">
<h2>Getting Started</h2>
<p>
This card contains a heading and a short description to demonstrate
nested content structure inside a container element.
</p>
</div>
</div>`;
const document = htmlToStoryblokRichtext(html, {
parsers: {
blok: {
parseHTML: () => [{ tag: "div.card" }],
attributeParsers: {
body: (el) => {
const title = el.querySelector("h2")?.textContent ?? "";
const description = el.querySelector("p")?.textContent ?? "";
return [
{
title,
description,
component: "card",
},
];
},
},
},
},
});

As with rich text rendering, full type safety is provided for all supported nodes and marks when creating custom parsers. Each supported parser can define the following options:

  • parseHTML - Defines one or more parsing rules used to identify matching HTML elements.
  • attributeParsers - Defines how attributes should be extracted and mapped to rich text node attributes.

In the previous example, a div element with the card class is mapped to a Storyblok component. The parser extracts the title and description from the HTML structure and stores them in the body attribute of the resulting blok node.

The following example demonstrates how to override the default link parser, allowing you to customize how link elements are converted into Storyblok rich text link attributes.

import { htmlToStoryblokRichtext, mapToAttribute } from "@storyblok/richtext/html-parser";
const html = `<a
href="/documentation/getting-started"
target="_self"
title="View the Getting Started guide"
aria-label="Navigate to the Getting Started guide"
>
Getting Started
</a>`;
const result = htmlToStoryblokRichtext(html, {
parsers: {
link: {
attributeParsers: {
href: mapToAttribute("href"),
target: mapToAttribute("target"),
linktype: (el) => {
const href = el.getAttribute("href") || "";
if (href.startsWith("http")) {
return "url";
} else if (href.startsWith("mailto:")) {
return "email";
} else if (href.startsWith("/")) {
return "story";
}
return null;
},
custom: (el) => ({
title: mapToAttribute("title")(el),
ariaLabel: mapToAttribute("aria-label")(el),
}),
},
},
},
});

The mapToAttribute helper is provided for mapping HTML attributes directly to rich text node attributes. This can significantly reduce boilerplate when multiple attributes need to be extracted.

The package includes a utility for converting Markdown content to Storyblok’s rich text format.

import { markdownToStoryblokRichtext } from "@storyblok/richtext/markdown-parser";
const markdown = `
# Main Heading
This is a **bold** paragraph with *italic* text.
- List item 1
- List item 2
> This is a blockquote
`;
const richtextDoc = markdownToStoryblokRichtext(markdown);
const html = renderRichText(richtextDoc);
document.getElementById("content").innerHTML = html;

Markdown parsing can be customized by providing parser overrides. For example, to parse all headings as level 5:

import { markdownToStoryblokRichtext } from "@storyblok/richtext/markdown-parser";
const richtextDoc = markdownToStoryblokRichtext(markdown, {
parsers: {
heading: {
attributeParsers: {
level: (el: HTMLElement) => 5,
},
},
},
});

The @storyblok/richtext package is framework-agnostic and can be used with any JavaScript frontend framework. The package itself returns an HTML string.

For framework-specific rendering, a StoryblokRichText component is provided by each SDK. These components integrate with the framework’s rendering system and provide an improved developer experience when rendering rich text content.

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window) . Terms of Service (opens in a new window) apply.