How to render HTML tags in Sanity block content instead of plain text?
I can see the issue here – you've got HTML markup stored inside a Portable Text block's text field, which is not how Portable Text is designed to work. The HTML is being treated as plain text because that's exactly what it is in this structure.
The Problem:
Your block has all the HTML (<p>, <ul>, <strong>, etc.) stored as a raw string in the text property of a single span. Portable Text doesn't parse or interpret HTML – it's a structured JSON specification where formatting is represented through marks, blocks, and annotations, not HTML tags.
Why You're Getting a Strange Array:
When you try to serialize this with a custom serializer, you're getting the expected Portable Text structure (an array of blocks containing text and objects), but the text itself contains unparsed HTML, which is why it looks odd.
Solutions:
1. Convert HTML to Portable Text (Recommended)
You need to convert your HTML into proper Portable Text structure. Sanity provides the @sanity/block-tools package for this:
import {htmlToBlocks} from '@sanity/block-tools'
import {Schema} from '@sanity/schema'
// Define your schema
const defaultSchema = Schema.compile({
name: 'myBlog',
types: [
{
type: 'object',
name: 'blogPost',
fields: [
{
title: 'Body',
name: 'body',
type: 'array',
of: [{type: 'block'}]
}
]
}
]
})
const blockContentType = defaultSchema
.get('blogPost')
.fields.find(field => field.name === 'body').type
const htmlString = "<p><span style=\"text-decoration: underline;\">Your HTML here...</span></p>"
const blocks = htmlToBlocks(htmlString, blockContentType)2. If You Must Render HTML Directly
If you absolutely need to render the HTML as-is (though this defeats the purpose of Portable Text), you could create a custom serializer:
import {PortableText} from '@portabletext/react'
const components = {
types: {
block: ({value}) => {
// Check if the text contains HTML
const text = value.children?.[0]?.text || ''
if (text.includes('<')) {
return <div dangerouslySetInnerHTML={{__html: text}} />
}
// Otherwise render normally
return <p>{text}</p>
}
}
}
<PortableText value={yourContent} components={components} />⚠️ Warning: Using dangerouslySetInnerHTML opens you up to XSS vulnerabilities and removes the benefits of using Portable Text.
3. Fix at the Source (Best Long-term Solution)
The best solution is to prevent HTML from being stored in your Portable Text fields in the first place. Make sure your Studio schema is properly configured to use the block editor, which will create proper Portable Text structure with marks and decorators instead of raw HTML.
The HTML in your example should be represented as multiple blocks with proper marks (bold, italic, underline) rather than as a single text string containing HTML tags. This is the whole point of Portable Text – to maintain structured, platform-agnostic content that can be rendered consistently across different platforms.
Show original thread5 replies
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.