Generate table of contents from Portable Text with block-content-to-react

8 replies
Last updated: Jul 1, 2020
Hi everyone. I want to create table of content for a blog post and I'm using portable text to render the post using block-content-to-react. Is there a plugin/lib that can generate that from the portable text ? I couldn't find one. Has anyone else already solved this ?
AI Update

Great question! There isn't an official plugin specifically for generating a table of contents from Portable Text, but the community has developed a solid pattern for this with block-content-to-react.

The approach involves rendering your Portable Text twice - once to extract the headings for your TOC, and once to display the actual content. Check out this CodeSandbox example that demonstrates the technique perfectly.

Here's how it works:

Create a custom serializer for extracting headings:

const ToCSerializer = {
  types: {
    block: props => {
      const { style = 'normal' } = props.node;
      
      // Only render heading blocks
      if (/^h\d/.test(style)) {
        const level = style.replace(/[^\d]/g, '');
        return (
          <a href={`#${props.node._key}`}>
            {props.children}
          </a>
        );
      }
      
      // Return null for everything else
      return null;
    }
  }
};

// Render twice:
// 1. For the table of contents
<BlockContent blocks={content} serializers={ToCSerializer} />

// 2. For the main content with matching IDs
<BlockContent blocks={content} serializers={mainSerializers} />

Alternative approach with GROQ: You can also extract headings server-side using GROQ with the pt::text function to get plain text from your Portable Text headings:

*[_type == "post"] {
  title,
  "headings": body[style in ["h2", "h3"]] {
    "text": pt::text(@),
    style,
    _key
  }
}

This gives you structured heading data to build your TOC without parsing content client-side.

Important note: If you're starting fresh, consider using the newer @sanity/react-portable-text package instead of block-content-to-react. It's the current recommended approach and uses a components prop instead of serializers, but the same pattern applies.

One gotcha mentioned in the thread: if you add lists or other content and they're showing up in your TOC even though you're returning null, make sure you're also handling list types in your serializer - lists have their own type separate from blocks.

Show original thread
8 replies
user Y
yeah something like this....the links are not working on clicking in the codesandbox...but I get the idea. Thanks !
It’s because it’s in a iframe I think. Should work in the standalone window :)
user Y
This might be a bug
If you add a list in block content under h2 it is still showing in the serialized component even when I'm returning null for everything except header tags
user Y
This might be a BUG. I'm using the above example and If I add a list after header tag no matter where ...it shows up in the serialized content even though I'm returning
null
for everything except header tags in
ToCserializer
.
I reproduced it here :
https://codesandbox.io/s/linked-headings-with-portable-text-and-react-zy487?file=/src/index.js
user M
can u help with ^ ? Am I doing something wrong or is this a bug?

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?