Roles and authentication
Structured content
Was this page helpful?
Learn how to seamlessly migrate rich text with inline styles (like bold, italic, and underline) into Sanity block content. This guide provides a step-by-step solution to handle custom deserialization, including nested spans and advanced inline styles.
This developer guide was contributed by KJ O'Brien (Senior Support Engineer).
When migrating rich text content from other CMS platforms into Sanity, you may encounter inline styles like <span style="font-weight: bold;">. By default, Sanity's html-to-blocks method doesn't handle these styles, leading to loss of formatting in the converted content. This guide walks you through creating custom deserialization rules to properly handle these cases. We'll also cover how to process nested spans, preserve spaces between words, and merge decorators effectively. By the end, you'll have a robust solution for rich text migration without losing valuable formatting.
When migrating rich text content from another CMS to Sanity, inline styles (e.g., <span style="font-weight: bold;">) often need to be translated into decorators like strong, em, or underline. This guide walks you through customizing the html-to-blocks serialization to handle such cases, including nested spans with multiple styles.
@sanity/block-tools package.By default, the html-to-blocks method handles common tags like <strong> and <em>, but inline styles like <span style="font-weight: bold;"> are ignored. To convert these spans into appropriate decorators, we need to extend the deserialization rules.
Custom Deserialization Rules
The following code demonstrates how to handle spans with inline styles, including nested spans:
const customRules = [
{
deserialize(el, next) {
if (el.tagName === 'SPAN') {
const style = el.style
const marks = []
// Collect marks from inline styles
if (style?.fontWeight === 'bold' || style?.fontWeight >= 600) {
marks.push('strong')
}
if (style?.fontStyle === 'italic') {
marks.push('em')
}
if (style?.textDecoration.includes('underline')) {
marks.push('underline')
}
// Initialize an array to hold the final processed spans
const processedSpans = []
// Process child nodes recursively
Array.from(el.childNodes).forEach((node) => {
if (node.nodeType === 3) {
// Handle text nodes
const text = node.nodeValue
if (text) {
processedSpans.push({
_type: 'span',
text,
marks,
})
}
} else {
// Process child elements recursively
const childNodes = next([node]).map((child) => {
if (child._type === 'span') {
return {
...child,
marks: [...new Set([...(child.marks || []), ...marks])],
}
}
return child
})
processedSpans.push(...childNodes)
}
})
return processedSpans
}
return undefined // Pass to the next rule if not a span
},
},
]font-weight, font-style, and text-decoration styles into Sanity decorators.Set to ensure each mark is applied only once.style property of the <span> tag is inspected to determine which marks to apply.To ensure your custom rules work as expected, test the following HTML input:
<span style="font-weight: bold;">Want to <span style="font-style: italic;">learn <span style="text-decoration: underline;">a lot</span> more</span></span>
This should output:
[
{
"_type": "span",
"text": "Want to ",
"marks": ["strong"]
},
{
"_type": "span",
"text": "learn ",
"marks": ["strong", "em"]
},
{
"_type": "span",
"text": "a lot",
"marks": ["strong", "em", "underline"]
},
{
"_type": "span",
"text": " more",
"marks": ["strong", "em"]
}
]This approach allows for seamless migration of rich text content with inline styles into Sanity's block content, enabling you to preserve the original formatting. For additional details, refer to the @sanity/block-tools documentation.
const customRules = [
{
deserialize(el, next) {
if (el.tagName === 'SPAN') {
const style = el.style
const marks = []
// Collect marks from inline styles
if (style?.fontWeight === 'bold' || style?.fontWeight >= 600) {
marks.push('strong')
}
if (style?.fontStyle === 'italic') {
marks.push('em')
}
if (style?.textDecoration.includes('underline')) {
marks.push('underline')
}
// Initialize an array to hold the final processed spans
const processedSpans = []
// Process child nodes recursively
Array.from(el.childNodes).forEach((node) => {
if (node.nodeType === 3) {
// Handle text nodes
const text = node.nodeValue
if (text) {
processedSpans.push({
_type: 'span',
text,
marks,
})
}
} else {
// Process child elements recursively
const childNodes = next([node]).map((child) => {
if (child._type === 'span') {
return {
...child,
marks: [...new Set([...(child.marks || []), ...marks])],
}
}
return child
})
processedSpans.push(...childNodes)
}
})
return processedSpans
}
return undefined // Pass to the next rule if not a span
},
},
]<span style="font-weight: bold;">Want to <span style="font-style: italic;">learn <span style="text-decoration: underline;">a lot</span> more</span></span>[
{
"_type": "span",
"text": "Want to ",
"marks": ["strong"]
},
{
"_type": "span",
"text": "learn ",
"marks": ["strong", "em"]
},
{
"_type": "span",
"text": "a lot",
"marks": ["strong", "em", "underline"]
},
{
"_type": "span",
"text": " more",
"marks": ["strong", "em"]
}
]