Rendering Sanity Portable Text Safely With Custom Serializers
Learn how to render Sanity Portable Text safely by aligning schema, GROQ projection, and a custom serializer to preserve links, annotations, and block semantics.
02 Nov 2025, 18:05 UTC

Use a serializer to render Portable Text safely
The useful takeaway is to keep three pieces aligned: a Studio schema that declares a block array, a GROQ projection that returns children and markDefs, and a frontend serializer that maps block types and marks to your components. Do not render the raw JSON as HTML.
Studio schema field
{
name: 'body',
title: 'Body',
type: 'array',
of: [
{
type: 'block',
styles: [{ title: 'Normal', value: 'normal' }, { title: 'Heading 2', value: 'h2' }],
lists: [{ title: 'Bullet', value: 'bullet' }]
}
]
}
Save this object in a schema file (e.g., schemas/documents/article.js) and run sanity start from the Studio directory to validate. You need read/write permission on the dataset to publish.
GROQ query that preserves structure
*[_type == "article" && _id == $id]{
body[]{
_type == 'block' => {
_key,
style,
listItem,
children[]{ text, marks },
markDefs[]
}
}
}
Run the query with the Sanity Client (sanity-client) or via sanity fetch. Expected result: an array where each object has _type === 'block', a children array of spans, and a markDefs array. If markDefs is missing, links cannot be resolved.
React serializer example
// serializers.js
export const serializers = {
marks: {
strong: ({ children }) => {children},
em: ({ children }) => {children},
code: ({ children }) => {children},
link: ({ mark, children }) => {
const href = mark?.href ?? '#';
return {children};
}
},
types: {
block: ({ node, children }) => {
const Tag = node.style === 'h2' ? 'h2' : 'p';
return {children};
}
}
};
Use it with @sanity/block-content-to-react or a custom renderer:
import BlockContent from '@sanity/block-content-to-react';
import { serializers } from './serializers';
function ArticleBody({ body }) {
return ;
}
After rendering, inspect the DOM: bold text appears as <strong>, links have an href attribute, and heading levels map to <h2> when the style is h2.
Limits and common mistakes
- Do not treat Portable Text as raw HTML. Skipping the serializer loses links, annotations, and block semantics.
- Images and files inside blocks are separate asset documents. They appear as
{ _type: 'image', asset: { _ref: '...' } }insidechildrenand must be resolved through the Sanity asset pipeline or CDN; the block serializer alone does not produce image URLs. - Unresolved
markDefsbreak links. A mark on a span references amarkDefby key; if the projection omitsmarkDefsor the data is incomplete, the link target is unknown. - Schema differences between Sanity Studio v2 and v3 affect validation but not the block field shape. Confirm your version with
sanity --versionin the Studio directory before applying the schema.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.