Add Table of Contents to Blog with Sanity and Next.js

14 replies
Last updated: Nov 29, 2025
help 👌Hello, please does anyone have an idea about how I can add ✔️`table of content`✔️ to my blog with
sanity
and
NextJs
.Something like this.
AI Update

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 thread
14 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.

Was this answer helpful?