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:
The table of contents component itself is an ordered list (
<ol>
) that links to each section header in the post.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">
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):
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 h2
s, 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.