
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeGreat 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.
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