HTML
@portabletext/to-html renders Portable Text to a plain HTML string. It has no framework dependency and works anywhere JavaScript runs: Node.js, Deno, edge runtimes, and browsers.
Use it for server-side rendering pipelines, static site generators, email templates, or any context where you need a string rather than a component tree.
Install
Section titled “Install”npm i @portabletext/to-htmlpnpm add @portabletext/to-htmlyarn add @portabletext/to-htmlBasic usage
Section titled “Basic usage”toHTML takes an array of Portable Text blocks and an options object. It returns an HTML string.
import {toHTML} from '@portabletext/to-html'
const html = toHTML(portableTextBlocks, { components: { /* optional: override or extend default components */ },})The components option is where you register renderers for custom types and marks. Without it, toHTML uses the built-in defaults for standard block styles, lists, and decorators.
Custom components
Section titled “Custom components”Custom link mark with URI safety checking
Section titled “Custom link mark with URI safety checking”When rendering links from user-supplied content, you should validate the href before emitting it. @portabletext/to-html exports uriLooksSafe() for exactly this purpose.
The example below also uses htm and vhtml to construct HTML safely without string concatenation. See the note on safe HTML construction below.
import {toHTML, uriLooksSafe} from '@portabletext/to-html'import htm from 'htm'import vhtml from 'vhtml'
const html = htm.bind(vhtml)
const components = { types: { image: ({value}) => html`<img src="${value.imageUrl}" alt="${value.alt ?? ''}" />`, callToAction: ({value, isInline}) => isInline ? html`<a href="${value.url}">${value.text}</a>` : html`<div class="callToAction">${value.text}</div>`, }, marks: { link: ({children, value}) => { const href = value.href || '' if (uriLooksSafe(href)) { const rel = href.startsWith('/') ? undefined : 'noreferrer noopener' return html`<a href="${href}" rel="${rel}">${children}</a>` } return children }, },}
const result = toHTML(portableTextBlocks, {components})The link mark renderer above:
- Checks the
hrefwithuriLooksSafe()before rendering it. - Adds
rel="noreferrer noopener"on external links (anyhrefthat does not start with/). - Returns
childrenunwrapped if the URI fails the safety check, rather than rendering a broken or dangerous link.
Safe HTML construction
Section titled “Safe HTML construction”Component functions return strings. You can use template literals directly, but that requires careful manual escaping of every interpolated value. A safer approach is to use htm + vhtml:
htmprovides JSX-like tagged template syntax.vhtmlrenders virtual DOM nodes to an HTML string, escaping attribute values and text content automatically.
Bind them once at the top of your file:
import htm from 'htm'import vhtml from 'vhtml'
const html = htm.bind(vhtml)Then use html`...` in your component functions instead of raw string interpolation.
API reference
Section titled “API reference”toHTML(blocks, options)
Section titled “toHTML(blocks, options)”| Parameter | Type | Description |
|---|---|---|
blocks | PortableTextBlock | PortableTextBlock[] | The Portable Text content to render. |
options.components | PortableTextHtmlComponents | Component map. Keys listed below. |
options.onMissingComponent | function | false | Called when a component is not found. Pass false to silence warnings. |
Component map keys
Section titled “Component map keys”| Key | Renders | Receives |
|---|---|---|
types | Custom object types (block or inline) | { value, isInline } |
marks | Decorators and annotations | { markType, value, children } |
block | Block styles (normal, h1-h6, blockquote) | { value, children } |
list | List containers (bullet, number) | { value, children } |
listItem | List items | { value, children } |
hardBreak | Line breaks within spans (default: <br />) | (none) |
unknownMark | Fallback for unregistered marks | { markType, value, children } |
unknownType | Fallback for unregistered types | { value, isInline } |
unknownBlockStyle | Fallback for unregistered block styles | { value, children } |
unknownList | Fallback for unregistered list styles | { value, children } |
unknownListItem | Fallback for unregistered list item styles | { value, children } |
The unknown* keys let you define graceful fallbacks instead of relying on the default behavior (which logs a warning and renders nothing or plain text).
Exported utilities
Section titled “Exported utilities”import { defaultComponents, // built-in component implementations escapeHTML, // HTML-escape a raw string toHTML, // main render function uriLooksSafe, // URI safety checker} from '@portabletext/to-html'| Export | Description |
|---|---|
toHTML | Renders Portable Text blocks to an HTML string. |
uriLooksSafe | Returns true if a URI does not use a dangerous scheme. Use this before rendering any user-supplied href. |
escapeHTML | Escapes &, <, >, ", and ' in a raw string. Useful when building component functions without vhtml. |
defaultComponents | The built-in component map. Spread and override individual keys to extend defaults rather than replace them. |
Missing component warnings
Section titled “Missing component warnings”By default, toHTML logs a warning to the console when it encounters a type or mark with no registered component. You can route these warnings to your own logger or silence them entirely.
toHTML(blocks, { onMissingComponent: (message, {type, nodeType}) => { myLogger.warn(message, {type, nodeType}) },})
// Or silence entirely:toHTML(blocks, {onMissingComponent: false})nodeType is one of 'block', 'mark', 'blockStyle', 'listStyle', or 'listItemStyle'. Use it to filter which missing components you care about.
Further reading
Section titled “Further reading”Full API documentation, changelog, and source: @portabletext/to-html on GitHub.