Generate table of contents from Portable Text with block-content-to-react
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 thread8 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.