
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeLooking at your code, the issue is that you're trying to use hyperscript to handle HTML strings, but in a server-side rendering context with @sanity/block-content-to-html, you need to either return raw HTML strings or properly handle the recursive serialization.
The problem is that when you call blockContentToHtml() on props.node.text, it returns an HTML string, but hyperscript treats strings as text content (escaping them) rather than parsing them as HTML. Hyperscript in server-side contexts doesn't have an innerHTML property like browser DOM elements do.
Here's the correct approach - return raw HTML strings from your serializer instead of mixing hyperscript with HTML strings:
const blocksToHtml = require('@sanity/block-content-to-html')
function ImageBlockLeftSerializer(props) {
const imageUrl = getImageUrl({ node: props.node.image, options: props.options })
// Recursively render the nested block content
const nestedHtml = blocksToHtml({
blocks: props.node.text,
serializers: props.serializers, // Pass through serializers for recursive handling
projectId: props.options.projectId,
dataset: props.options.dataset,
imageOptions: props.options.imageOptions
})
// Return raw HTML string instead of hyperscript
return `
<div class="row">
<div class="col-12 col-md-6 d-flex align-items-center">
<img src="${imageUrl}" />
</div>
<div class="col-12 col-md-6">
${nestedHtml}
</div>
</div>
`
}The @sanity/block-content-to-html library can handle serializers that return either hyperscript objects or HTML strings, so this will work correctly. The key points:
blocksToHtml() recursively with props.node.text as the blocksImportant migration note: The @sanity/block-content-to-html package is now deprecated. When you have time to migrate, switch to the modern @portabletext/to-html library:
const {toHTML} = require('@portabletext/to-html')
const components = {
types: {
imageBlockLeft: ({value}) => {
const imageUrl = getImageUrl(value.image)
const nestedHtml = toHTML(value.text, {components}) // Recursive call
return `
<div class="row">
<div class="col-12 col-md-6 d-flex align-items-center">
<img src="${imageUrl}" />
</div>
<div class="col-12 col-md-6">
${nestedHtml}
</div>
</div>
`
}
}
}
const html = toHTML(yourBlockContent, {components})The main differences: you access the node via value instead of node, use components instead of serializers, and the API is cleaner overall. The recursive pattern remains the same—call the renderer with the nested content and pass through your components configuration.
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store