Skip to content

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.

Terminal window
npm i @portabletext/to-html

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.

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:

  1. Checks the href with uriLooksSafe() before rendering it.
  2. Adds rel="noreferrer noopener" on external links (any href that does not start with /).
  3. Returns children unwrapped if the URI fails the safety check, rather than rendering a broken or dangerous link.

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:

  • htm provides JSX-like tagged template syntax.
  • vhtml renders 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.

ParameterTypeDescription
blocksPortableTextBlock | PortableTextBlock[]The Portable Text content to render.
options.componentsPortableTextHtmlComponentsComponent map. Keys listed below.
options.onMissingComponentfunction | falseCalled when a component is not found. Pass false to silence warnings.
KeyRendersReceives
typesCustom object types (block or inline){ value, isInline }
marksDecorators and annotations{ markType, value, children }
blockBlock styles (normal, h1-h6, blockquote){ value, children }
listList containers (bullet, number){ value, children }
listItemList items{ value, children }
hardBreakLine breaks within spans (default: <br />)(none)
unknownMarkFallback for unregistered marks{ markType, value, children }
unknownTypeFallback for unregistered types{ value, isInline }
unknownBlockStyleFallback for unregistered block styles{ value, children }
unknownListFallback for unregistered list styles{ value, children }
unknownListItemFallback 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).

import {
defaultComponents, // built-in component implementations
escapeHTML, // HTML-escape a raw string
toHTML, // main render function
uriLooksSafe, // URI safety checker
} from '@portabletext/to-html'
ExportDescription
toHTMLRenders Portable Text blocks to an HTML string.
uriLooksSafeReturns true if a URI does not use a dangerous scheme. Use this before rendering any user-supplied href.
escapeHTMLEscapes &, <, >, ", and ' in a raw string. Useful when building component functions without vhtml.
defaultComponentsThe built-in component map. Spread and override individual keys to extend defaults rather than replace them.

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.

Full API documentation, changelog, and source: @portabletext/to-html on GitHub.