Add Table of Contents to Blog with Sanity and Next.js
Creating 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 Basic Approach
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.
Implementation Steps
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} />Complete Example
export default function BlogPost({ post }) {
const headings = getHeadings(post.body);
return (
<article>
<TableOfContents headings={headings} />
<PortableText
value={post.body}
components={components}
/>
</article>
);
}Styling Tips
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!
Show original thread14 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.