
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeCreating a table of contents for your blog posts with Sanity and Next.js involves extracting heading blocks from your Portable Text content and rendering them as navigation links. Here's how to approach it:
The key is to process your Portable Text content twice: once to extract headings for the table of contents, and once to render the actual content. There's a helpful CodeSandbox example shared in the Sanity community that demonstrates this pattern.
1. Extract headings from your Portable Text:
Create a function that filters through your Portable Text blocks to find heading blocks (h2, h3, etc.):
function getHeadings(portableTextContent) {
return portableTextContent
.filter(block => block.style && block.style.match(/^h\d/))
.map(heading => ({
style: heading.style,
text: heading.children[0].text,
_key: heading._key
}));
}2. Render the table of contents:
Use the extracted headings to create your navigation:
function TableOfContents({ headings }) {
return (
<nav>
<ul>
{headings.map(heading => (
<li key={heading._key}>
<a href={`#${heading._key}`}>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
);
}3. Add IDs to your rendered headings:
When rendering your main content with @portabletext/react, customize the heading serializers to include IDs that match your table of contents links:
import { PortableText } from '@portabletext/react';
const components = {
block: {
h2: ({value, children}) => (
<h2 id={value._key}>{children}</h2>
),
h3: ({value, children}) => (
<h3 id={value._key}>{children}</h3>
),
// Add other heading levels as needed
}
};
<PortableText value={post.body} components={components} />export default function BlogPost({ post }) {
const headings = getHeadings(post.body);
return (
<article>
<TableOfContents headings={headings} />
<PortableText
value={post.body}
components={components}
/>
</article>
);
}You can enhance your table of contents with indentation based on heading levels:
nav ul {
list-style: none;
}
nav li[data-level="h3"] {
padding-left: 1rem;
}
nav li[data-level="h4"] {
padding-left: 2rem;
}The beauty of this approach is that it leverages Portable Text's structured nature - each block has a unique _key that you can use for linking, and the heading styles are explicitly defined in the data structure, making extraction straightforward. No additional plugins needed!
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