@storyblok/richtext (Version 4.x)
@storyblok/richtext is a custom resolver for Storyblok rich text in JavaScript applications.
Installation
Section titled “Installation”Add the package to a project by running this command in the terminal:
npm install @storyblok/richtext@latestimport { richTextResolver } from "@storyblok/richtext";const { render } = richTextResolver();const html = render(doc);document.querySelector("#app").innerHTML = `<div>${html}</div>`;Custom Tiptap extensions
Section titled “Custom Tiptap extensions”The recommended way to customize rendering is via the tiptapExtensions option. Extensions follow the Tiptap extension API and work for both parsing and rendering.
Add a custom node
Section titled “Add a custom node”Create a custom node as demonstrated in the example below:
import { Node } from "@tiptap/core";import { richTextResolver } from "@storyblok/richtext";
const Callout = Node.create({ name: "callout", group: "block", content: "inline*", parseHTML() { return [{ tag: "div[data-callout]" }]; }, renderHTML({ HTMLAttributes }) { return ["div", { "data-callout": "", class: "callout", ...HTMLAttributes }, 0]; },});
const { render } = richTextResolver({ tiptapExtensions: { callout: Callout },});
const html = render(doc);Use the same extension to parse HTML via htmlToStoryblokRichtext:
import { htmlToStoryblokRichtext } from "@storyblok/richtext/html-parser";
const json = htmlToStoryblokRichtext(html, { tiptapExtensions: { callout: Callout },});Override a built-in extension
Section titled “Override a built-in extension”Pass an extension with the same key as a built-in to replace it. Use .extend() on a Tiptap extension to inherit its parseHTML logic and only override renderHTML:
import Heading from "@tiptap/extension-heading";import { richTextResolver } from "@storyblok/richtext";
const CustomHeading = Heading.extend({ renderHTML({ node, HTMLAttributes }) { const level = node.attrs.level; return [`h${level}`, { class: `heading-${level}`, ...HTMLAttributes }, 0]; },});
const { render } = richTextResolver({ tiptapExtensions: { heading: CustomHeading },});Framework components with asTag
Section titled “Framework components with asTag”Tiptap’s renderHTML returns a DOMOutputSpec, a nested array where the first element is a string tag name like 'a' or 'div'. In framework SDKs, it is usually preferable to render a framework component instead.
The asTag helper casts a component reference to support TypeScript while the rich text renderer handles it correctly at runtime:
import { Mark } from "@tiptap/core";import { asTag } from "@storyblok/vue"; // also available from @storyblok/react, @storyblok/richtextimport { RouterLink } from "vue-router";
const CustomLink = Mark.create({ name: "link", renderHTML({ HTMLAttributes }) { if (HTMLAttributes.linktype === "story") { return [asTag(RouterLink), { to: HTMLAttributes.href }, 0]; } return ["a", { href: HTMLAttributes.href, target: HTMLAttributes.target }, 0]; },});
const { render } = richTextResolver({ tiptapExtensions: { link: CustomLink },});Blok component rendering
Section titled “Blok component rendering”Storyblok components are embedded in rich text as blok nodes. Each such node contains a body array with one or more components that need to be rendered as actual framework components, not as HTML tags.
@storyblok/richtext version 4.x handles this through ComponentBlok.configure({ renderComponent }), a callback on the blok extension that receives each blok and returns framework-native output. This is something usually done, if needed, at frontend framework level.
Segment rich text
Section titled “Segment rich text”The segmentStoryblokRichText utility splits a rich text document into an ordered list of HTML segments and blok segments. This is useful when framework-specific rendering of blok nodes alongside raw HTML is needed:
import { segmentStoryblokRichText } from "@storyblok/richtext";
const segments = segmentStoryblokRichText(doc);
for (const segment of segments) { if (segment.type === "html") { renderHtml(segment.content); } if (segment.type === "blok") { renderBlokComponent(segment.blok); }}Optimize images
Section titled “Optimize images”To optimize images, use the optimizeImages property on the richTextResolver options. For a full list of available options, refer to the Image Service documentation.
import { richTextResolver } from "@storyblok/richtext";
const html = richTextResolver({ optimizeImages: { 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, }, },}).render(doc);Markdown to rich text
Section titled “Markdown to rich text”The package includes a utility for converting Markdown content to Storyblok’s rich text format, which can be rendered via richTextResolver.
Supported markdown elements:
- Text formatting:
**bold**,*italic*,~~strikethrough~~,`code`,[links](url) - Headings:
# H1through###### H6 - Lists:
- unorderedand1. ordered listswith nesting - Code blocks:
```fenced```and indented blocks - Blockquotes:
> quoted text - Images:
 - Links:
[text](url)and[text](url "title") - Tables: Standard markdown table syntax
- Horizontal rules:
--- - Line breaks: (two spaces) for hard breaks
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 = richTextResolver().render(richtextDoc);document.getElementById("content").innerHTML = html;Customize how specific Markdown elements are parsed by providing custom Tiptap extensions:
import { markdownToStoryblokRichtext } from "@storyblok/richtext/markdown-parser";
const richtextDoc = markdownToStoryblokRichtext(markdown, { tiptapExtensions: { heading: CustomHeading },});HTML to Storyblok rich text
Section titled “HTML to Storyblok rich text”The package includes a utility for converting HTML content to Storyblok’s rich text format, which can be rendered via richTextResolver.
Supported HTML elements:
- Headlines:
<h1-6> - Paragraphs:
<p> - Lists:
<ol>,<ul>,<li> - Tables:
<table>,<thead>,<tbody>,<tr>,<th>,<td> - Blockquote:
<blockquote> - Code:
<pre>,<code> - Links:
<a> - Formatting:
<strong>,<b>,<em>,<i>,<del>,<s> - Images:
<img> - Misc:
<span>,<hr>,<br>
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 output = richTextResolver().render(richtextDoc);document.getElementById("content").innerHTML = output;Customize parsing by providing custom tiptap extensions:
import { Node } from "@tiptap/core";import { htmlToStoryblokRichtext } from "@storyblok/richtext/html-parser";
const Callout = Node.create({ name: "callout", group: "block", content: "inline*", parseHTML() { return [{ tag: "div[data-callout]" }]; }, renderHTML({ HTMLAttributes }) { return ["div", { "data-callout": "", class: "callout", ...HTMLAttributes }, 0]; },});
const richtextDoc = htmlToStoryblokRichtext(html, { tiptapExtensions: { callout: Callout },});Framework usage
Section titled “Framework usage”The @storyblok/richtext package is framework-agnostic and can be used with any JavaScript-based frontend framework. Below are examples of how to use the package with different frameworks.
import React from "react";import { richTextResolver } from "@storyblok/richtext";
const options: StoryblokRichTextOptions<ReactElement> = { renderFn: React.createElement, keyedResolvers: true,};
function Example({ doc }) { const html = richTextResolver(options).render(doc); return <>{formattedHtml}</>;}Refer to playground/react in the @storyblok/richtext package repository for a complete example.
<script setup> import type { VNode } from 'vue'; import { createTextVNode, h } from 'vue'; import { richTextResolver, type StoryblokRichTextOptions } from '@storyblok/richtext';
const options: StoryblokRichTextOptions<VNode> = { renderFn: h, textFn: createTextVNode, keyedResolvers: true, };
const root = () => richTextResolver<VNode>(options).render(doc);</script>
<template> <root /></template>Refer to playground/vue in the @storyblok/richtext package repository for a complete example.
TypeScript generics
Section titled “TypeScript generics”Correct type support in a framework-agnostic way is ensured by using Typescript generics, circumventing the need to import types and require framework packages as dependencies.
Vanilla: string
Section titled “Vanilla: string”import { Mark } from "@tiptap/core";import { richTextResolver } from "@storyblok/richtext";
const CustomLink = Mark.create({ name: "link", renderHTML({ HTMLAttributes }) { return ["a", { href: HTMLAttributes.href, class: "custom-link" }, 0]; },});
const html = richTextResolver<string>({ tiptapExtensions: { link: CustomLink },}).render(doc);React: React.ReactElement
Section titled “React: React.ReactElement”const options: StoryblokRichTextOptions<React.ReactElement> = { renderFn: React.createElement, keyedResolvers: true,};const root = () => richTextResolver<React.ReactElement>(options).render(doc);Vue: VNode
Section titled “Vue: VNode”const options: StoryblokRichTextOptions<VNode> = { renderFn: h, keyedResolvers: true,};const root = () => richTextResolver<VNode>(options).render(doc);Further resources
Section titled “Further resources”Previous versions
Section titled “Previous versions”Was this page helpful?
This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window) . Terms of Service (opens in a new window) apply.
Get in touch with the Storyblok community