Preserving HTML ID attributes in block content using the block tools library
Yes, you can preserve HTML id attributes in block content, but you'll need to store them as custom data on the block object itself. By default, htmlToBlocks doesn't preserve arbitrary HTML attributes like id because standard Portable Text blocks don't have a built-in place for them.
Here's how to solve this:
Solution: Store IDs as Custom Block Data
You can extend your htmlToBlocks rules to extract the id attribute and store it as a custom property on the block object:
import {htmlToBlocks} from '@sanity/block-tools'
import {JSDOM} from 'jsdom'
import {Schema} from '@sanity/schema'
// Define your schema with the custom id field
const defaultSchema = Schema.compile({
name: 'myBlog',
types: [
{
type: 'object',
name: 'blogPost',
fields: [
{
title: 'Body',
name: 'body',
type: 'array',
of: [
{
type: 'block',
// Add custom fields to your block type
fields: [
{
name: 'htmlId',
type: 'string',
title: 'HTML ID'
}
]
}
]
}
]
}
]
})
const blockContentType = defaultSchema
.get('blogPost')
.fields.find((field) => field.name === 'body').type
// Custom rules to preserve id attributes
const customRules = [
{
deserialize(el, next, block) {
// Handle any element with an id attribute
if (el.getAttribute && el.getAttribute('id')) {
const id = el.getAttribute('id')
// For block-level elements (p, h1-h6, etc.)
if (['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DIV'].includes(el.tagName)) {
const children = next(el.childNodes)
return {
...block(children),
htmlId: id // Store the id as custom data
}
}
}
return undefined
}
}
]
// Use it
const html = '<p id="important">Important paragraph</p>'
const blocks = htmlToBlocks(html, blockContentType, {
rules: customRules,
parseHtml: (html) => new JSDOM(html).window.document
})This will produce:
[
{
"_type": "block",
"markDefs": [],
"style": "normal",
"htmlId": "important",
"children": [
{
"_type": "span",
"marks": [],
"text": "Important paragraph",
"_key": "55a9bea915f10"
}
],
"_key": "55a9bea915f1"
}
]Alternative: Use Annotations for Inline Elements
If you need to preserve IDs on inline elements (like <span id="xyz">), you'd want to use annotations instead, which allow you to attach structured data to text spans through the markDefs array.
Schema Configuration
Make sure your Portable Text schema includes the custom field:
{
name: 'body',
type: 'array',
of: [
{
type: 'block',
fields: [
{
name: 'htmlId',
type: 'string',
title: 'HTML ID',
description: 'Preserved from original HTML'
}
]
}
]
}Rendering the IDs
When rendering your Portable Text on the frontend, you can access the custom htmlId property in your serializers to output the ID back to HTML. For example, with @portabletext/react:
<PortableText
value={content}
components={{
block: {
normal: ({value, children}) => (
<p id={value.htmlId}>{children}</p>
)
}
}}
/>This approach gives you full control over preserving and rendering HTML IDs while keeping your content structured and reusable across different platforms.
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.