11ty + Sanity: Serializer for custom block with nested Portable Text

17 replies
Last updated: Jan 28, 2026
Hello everyone,
I'm still battling with 11TY + Sanity and serializers.

I have an 11ty filter that is working with my serializers.

However, I can't make the serializer for my custom block type "infoText" work:


//from utils/serializers.js


const { h } = require("@sanity/block-content-to-html");


// Learn more on <https://www.sanity.io/docs/guides/introduction-to-portable-text>

module.exports = {
  
types: {
      
cta: ({ node }) => {

        _return_ h(
          
'a',
          
{
            
className:
              
'bg-yellow-500 text-white',
            
href: node.ctaUrl,
          
},
          
node.ctaText,
        
)
      
},
      
infoText: ({ node }) => {

        _return_ h(
          
'p',
          
{
            
className:
              
'bg-blue-500 text-white',
          
},
          
node.bodyInfo.children,
        
)
      
},
    
},
  
}

The node.bodyInfo.children does not work, nor any of the combinations I've tried. My post's data structure is as follows:


{
  "_createdAt": "2021-09-14T11:25:05Z",
  "_id": "89ff5403-326b-4db1-8752-04ea1c85f114",
  "_rev": "7dkOKJtWoyCn0kHUhHzZu7",
  "_type": "post",
  "_updatedAt": "2021-09-20T06:38:14Z",
  "body": [
    {
      "_key": "f84e932860bf",
      "_type": "block",
      "children": [
        {
          "_key": "bd29bce1dda1",
          "_type": "span",
          "marks": [],
          "text": ""
        }
      ],
      "markDefs": [
        {
          "_key": "38aa715c6214",
          "_type": "link",
          "href": "<https://www.fundacionrcoms.com/anti-edad/metodos-eliminar-arrugas/>"
        }
      ],
      "style": "normal"
    },
    {
      "_key": "bf5d17f3da91",
      "_type": "cta",
      "ctaText": "test",
      "ctaUrl": "<https://www.fundacionrcoms.com/>"
    },
    {
      "_key": "595873ddfc54",
      "_type": "block",
      "children": [
        {
          "_key": "ba794ddbef68",
          "_type": "span",
          "marks": [],
          "text": ""
        }
      ],
      "markDefs": [
        {
          "_key": "38aa715c6214",
          "_type": "link",
          "href": "<https://www.fundacionrcoms.com/anti-edad/metodos-eliminar-arrugas/>"
        }
      ],
      "style": "normal"
    },
    {
      "_key": "8acb94638c0c",
      "_type": "infoText",
      "bodyInfo": [
        {
          "_key": "6b6e533e67fd",
          "_type": "block",
          "children": [
            {
              "_key": "3593ad3abdf9",
              "_type": "span",
              "marks": [],
              "text": "test test info"
            }
          ],
          "markDefs": [],
          "style": "normal"
        }
      ]
    },
    .....etc
Any tips? There's not much info on this.
AI Update

I can see you're working with nested Portable Text in a custom block type using @sanity/block-content-to-html in your 11ty project. The issue is that bodyInfo contains an array of Portable Text blocks, not simple text, so you need to handle it differently.

First, it's important to note that @sanity/block-content-to-html is deprecated and the Sanity team recommends migrating to @portabletext/to-html. However, I'll help you solve this with your current setup.

The problem with your infoText serializer is that node.bodyInfo is an array of Portable Text blocks (you can see in your data it has the full block structure with _type: "block", children, etc.), so you can't just access .children directly.

Here's how to fix it with @sanity/block-content-to-html:

const blocksToHtml = require('@sanity/block-content-to-html');
const { h } = require("@sanity/block-content-to-html");

module.exports = {
  types: {
    cta: ({ node }) => {
      return h(
        'a',
        {
          className: 'bg-yellow-500 text-white',
          href: node.ctaUrl,
        },
        node.ctaText,
      )
    },
    infoText: ({ node }) => {
      // Render the nested Portable Text by calling blocksToHtml
      const innerHtml = blocksToHtml({
        blocks: node.bodyInfo,
        // You can pass the same serializers recursively if needed
        serializers: module.exports
      });
      
      return h(
        'div',
        {
          className: 'bg-blue-500 text-white',
        },
        innerHtml
      )
    },
  },
}

Key points:

  1. node.bodyInfo is an array of blocks, not just children. Looking at your data structure, it contains [{ _type: "block", children: [...], markDefs: [], style: "normal" }]

  2. You need to call blocksToHtml() recursively to serialize the nested Portable Text content

  3. I changed the wrapper from <p> to <div> because you're rendering block-level content inside it (paragraphs inside paragraphs isn't valid HTML)

  4. You can pass serializers: module.exports to ensure any custom serializers you've defined also apply to the nested content

Alternative: Extract plain text only

If you only want the plain text without any HTML structure from the nested content:

infoText: ({ node }) => {
  const plainText = node.bodyInfo
    .map(block => 
      block.children
        ?.map(child => child.text)
        .join('')
    )
    .join(' ');
  
  return h(
    'p',
    { className: 'bg-blue-500 text-white' },
    plainText
  )
}

This extracts just the text content ("test test info" in your example) without rendering it as HTML blocks.

Recommendation for the future:

Since @sanity/block-content-to-html is deprecated, consider migrating to @portabletext/to-html when you have time. The new package has better TypeScript support and a more modern API, though the concepts are similar.

Show original thread
17 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.

Was this answer helpful?