How to create a Table of Contents using Contentful & React

Will Elliott photoWill Elliott
React and contentful: how to create a table of contents

A Table of Contents (or "TOC") is almost a must-have for blogs these days. It's both helpful for SEO and is a better user experience.

Unfortunately, the Contentful headless CMS does not provide an easy way to create one out-of-the-box, so in this tutorial we're going to go over how to create a table of contents using @contentful/rich-text-react-renderer just like the one on this page.

How a Table of Contents works

From a technical perspective, it's pretty straight-forward. We should have:

  1. The table of contents component itself is an ordered list (<ol>) that links to each section header in the post.

  2. Each <li> in the ordered list should have an anchor tag linking to a respective section header using a "fragment" or "named anchor". This just means it has a hash appended to the front and is linking to a fragment of a page, not an entire page, e.g. <a href="#section-heading-1">

  3. The section headers themselves must now have a corresponding id attribute so the anchors know where to go: <h2 id="section-heading-1">

Filtering out the right data

First let's make sure we're importing the necessary packages. We'll need the following 3:

1import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
2import { BLOCKS } from '@contentful/rich-text-types'
3import slugify from 'slugify'

Necessary imports

Here's the structure that the Rich Text we receive from our headless CMS comes in. We'll want to specifically target and filter out h2 tags (sometimes h3 as well, depends on preference):

Rich text logged in console
Rich text logged in console

To grab our headers from this array, we're going to need two functions. The first will simply grab plain text from the heading-2 nodes we pass to it. The second will iterate over all of the nodes, filter out the h2s, and return an object for each that has both the plain text and the slugified version of this plain text.

1// This is for cases where the heading might be partially underlined and have two child nodes
2// so you can't just get the text (.value) from the first one in the array
3function getPlainTextFromHeader(contentNode) {
4  return contentNode.reduce((acc, current) => {
5    return acc + current.value
6  }, '')
7}
8
9function getHeadersFromRichText(richText) {
10  const headers = (content) => content.nodeType === BLOCKS.HEADING_2
11
12  return richText.content.filter(headers).map((heading) => {
13    const plainText = getPlainTextFromHeader(heading.content)
14
15    return {
16      text: plainText,
17      href: `#${slugify(plainText)}`,
18    }
19  })
20}
21
22// Results in something like:
23// [{ text: 'Heading one', slug: 'heading-one' }] 

Grabbing plain text value from the header

Creating the Table of Contents component

1const TableOfContents = ({ post }) => {
2  return (
3    <>
4      <h4>Table of Contents</h4>
5      <ol>
6        {getHeadersFromRichText(post.content).map((item, i) => (
7          <li key={i}>
8            <a href={item.href}>{item.text}</a>
9          </li>
10        ))}
11      </ol>
12    </>
13  )
14}

Table of contents component

This is the easy part. Now that we've got our data, we can just pass any post to the component and voila, we've got our Table of Contents. The last thing to do is make sure it's linking to the headers further down the page.

Rendering headers with ID attributes

As we talked about before, we need each header to have an id attribute so that our anchor fragments can link to them. That id will need to obviously match the slug that we've created in the table of contents:

1const richTextOptions = {
2  renderNode: {
3    [BLOCKS.HEADING_2]: (node, children) => {
4      const plainText = getPlainTextFromHeader(node.content)
5
6      return <h2 id={slugify(plainText)}>{children}</h2>
7    },
8  },
9}

Modify the header so that it has an ID attribute we can link to

Here we are using Contentful's renderer to override the default h2 and adding on that ID. Pretty straight forward.

Finally, in our post body, we'll pass the richTextOptions to our post component like so:

1<div className="post-content">
2    {documentToReactComponents(post.content, richTextOptions)}
3</div>

rendering content with rich text options

And that's about it!

Conclusion

To summarize, we were able to create a table of contents using React (and associated frameworks like Gatsby / Next.js) with Contentful by reconstructing some data, and passing it to our newly created Table of Contents component.