How to render HTML tags in Sanity block content instead of plain text?

5 replies
Last updated: Sep 23, 2021
Hi, I’m having an issue with this block:
 {
        "_key": "5vl1nzex72tktwgg1h4",
        "_type": "block",
        "children": [
            {
                "_key": "i8pefvhbtfktwgg1h4",
                "_type": "span",
                "marks": [],
                "text": "<p><span style=\"text-decoration: underline;\">A cult favorite, our Weekender is spacious enough for a long weekend or for days spent dreaming of one.</span></p>\n<p><em>Going the distance? Smart straps slip over the trolley of your carry-on for speedy getaways, and a removable woven shoulder strap offers hands-free flexibility. </em></p>\n<p><em><strong><span style=\"text-decoration: underline;\">Featuring leather accents that match our signature Ecocraft Canvas, made from 12 upcycled plastic water bottles, the Weekender is a natural companion when the weekend arrives or your flight out of town is ready for boarding.</span></strong></em></p>\n<p><strong>Example bullet point:</strong></p>\n<ul>\n<li><strong>1</strong></li>\n<li><strong>2</strong></li>\n<li><strong>3</strong></li>\n<li><strong>4</strong></li>\n</ul>\n<p><strong>Example Numbered points</strong>:</p>\n<ol>\n<li>bullet</li>\n<li>bullet</li>\n<li>bullet</li>\n<li>bullet</li>\n</ol>\n<p>Dimensions: 15\" x 8\" x 15\"<br><strong>Meets TSA carry-on requirements</strong></p>\n<p> </p>\n<p> </p>\n<p> </p>\n<p> </p>\n<p> </p>\n<p> </p>"
            }
        ],
        "markDefs": []
    }
I want to render the html as html not as text. When I try to create my own serializer I get a strange array that includes text and objects.
AI Update

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 thread
5 replies
Help me understand your use case: Is this HTML just pasted directly into the Portable Text editor?
it’s actually substituted in our code, but I believe the use case would be the same if you paste it in the editor
So how does the HTML come into the PT-array?
key gets sent down and we then replace it for this value that we pull from elsewhere
Sorry, just trying to understand further (if you have code I can look at, that would be helpful): So your editors puts in a key or something, and then you match and replace the
text
value in a middle-layer somewhere?

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.

Was this answer helpful?