Sanity Portable Text Rendering and Serializer Trade-offs
Portable Text stores rich content as JSON blocks. Rendering it correctly means writing serializers for marks and custom blocks, handling image asset references, and accepting payload trade-offs.
26 Jul 2025, 13:15 UTC

Editors see rich text, images and callouts in Sanity Studio, but the frontend renders plain paragraphs or missing media. The problem is not a data loss. Portable Text stores content as a JSON array of block objects, not HTML. Rendering requires an explicit mapping from those blocks and marks to React components.
Portable Text stores structure, not markup
Portable Text is Sanity's rich-text format. A field is an array where each element is a block with _type, style, and children. Children are spans with text and optional marks for annotations like bold or links. Custom blocks such as image or code are also blocks with their own _type and fields.
Sanity Studio's editor is built on Portable Text, and schemas can define custom block types beyond the default text, image and code blocks. Because the shape is JSON, GROQ can project the field and filter by _type or mark, but the field remains an opaque array until the frontend parses it.
You can inspect the shape without code. In Sanity Studio open the Vision plugin, run a query like:
*[_type == "article" && _id == ""]{ body }
Run this in Vision with read access to the dataset. Expected check: body is an array of objects with _type: "block", style, and children. Each child has text and an optional marks array. No rendering happens here, only structure verification.
Mapping blocks to React with serializers
Frontend rendering commonly uses @sanity/block-content-to-react. It walks the block array and delegates rendering via a serializers map. Serializers must exist for every custom block type and mark used in content; unknown blocks fall back to plain text or are omitted.
A minimal serializer setup for a link mark:
import BlockContent from '@sanity/block-content-to-react'
const serializers = {
marks: {
link: ({mark, children}) => {
const href = mark.href
return <a href={href} rel="noopener noreferrer" target="_blank">{children}</a>
}
}
}
<BlockContent blocks={article.body} serializers={serializers} />
Place this in a React component that receives article data from your Sanity client. Required permission is read access to the dataset via the client token. Risk: deploying a new custom block type in Studio before adding a matching serializer causes that block to disappear from the page.
Image blocks inside Portable Text
Image blocks inside Portable Text store only an asset reference. Hotspot and crop metadata live on the Sanity image asset document and are applied with @sanity/image-url.
Example serializer for an image block:
import imageUrlBuilder from '@sanity/image-url'
const builder = imageUrlBuilder({projectId: '', dataset: ''})
function imageBuilder(source){ return builder.image(source) }
serializers.types.image = ({node}) => {
const url = imageBuilder(node.asset).width(800).url()
return <img src={url} alt={node.alt || ''} />
}
The builder resolves variants at request time. Image URL variants generated by @sanity/image-url may incur CDN cache misses for uncached crops.
Trade-offs and how to verify
Large Portable Text documents increase payload size and client parsing cost, especially with many nested blocks or inline marks. Using dangerouslySetInnerHTML on Portable Text bypasses the serializer ecosystem and loses custom block and mark handling.
Adding a custom block type requires coordinated changes in Sanity Studio schema and frontend serializers. Mismatched deployments can break rendering.
Practical checks:
- Vision plugin: confirm the field is an array of block objects with children and marks.
- Render a Portable Text field with a custom serializer for a link mark and verify the rendered anchor attributes contain href.
- Pass an image asset reference from a Portable Text block to @sanity/image-url and confirm the output URL contains crop and hotspot parameters.
- Run a GROQ query that selects a Portable Text field and inspect the response to ensure block arrays and mark definitions are returned intact.
Keep serializers versioned with schema changes, limit block depth for long articles, and generate image URLs with explicit width and format to reduce payload.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.