@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@4The following examples show how to render rich text and convert Markdown or HTML to rich text.
import { 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’s 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”Rich text embeds Storyblok components as blok nodes. Each blok node contains a body array with one or more components, which the app must render as 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. Framework SDKs typically configure this callback, when needed.
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-performant-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 richTextResolver can then render.
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 - Block quotes:
> 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;To customize how the parser handles specific Markdown elements, provide 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 richTextResolver can then render.
Supported HTML elements:
- Headlines:
<h1-6> - Paragraphs:
<p> - Lists:
<ol>,<ul>,<li> - Tables:
<table>,<thead>,<tbody>,<tr>,<th>,<td> - Block quotes:
<blockquote> - Code:
<pre>,<code> - Links:
<a> - Formatting:
<strong>,<b>,<em>,<i>,<del>,<s> - Images:
<img> - Other elements:
<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;To customize parsing, provide 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 works with any JavaScript-based frontend framework. The following examples show how to use the package with different frameworks.
For a better developer experience, use the corresponding APIs available in the framework SDK, such as the StoryblokRichText component. For details, refer to the @storyblok/react reference.
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.
For a better developer experience, use the corresponding APIs available in the framework SDK, such as the StoryblokRichText component. For details, refer to the @storyblok/vue reference.
<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”TypeScript generics provide correct type support in a framework-agnostic way, without the need to import types or 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