Table of contents helps the visitors quickly get the summary of the article, and navigate to the sections that they are interested in.
In this article, I'll show you how to create a table of contents using Tocbot with Contentlayer and Next.js.
This tutorial is designed for people who use Next.js, Contentlayer, and markdown or MDX in their website.
What we build
Before we start, I'll show you what kind of a table of contents we are gonna create in this article.
- You can place it anywhere (in the blog contents, or a sidebar, etc)
- Smooth scrolling to the anchor point when clicked
- Offset value is available if you have a sticky header
- Choose the levels of heading tags you want to show
- Set an
activeclass to highlight the current section on scroll
Basically, we are gonna create the same table of contents you see on the sidebar in this page. (If you don't see it, increase your browser width to 1280px or up)
If this is what you wanna build, then let's get started!
Step 1. Install packages
First, we need to install the following packages.
npm install rehype-slug tocbotThese are all we need. I'll explain what these packages will do later on.
Step 2. Add ids to heading tags using rehype-slug
In order to set anchor links to heading tags, each one must have an id attribute with a unique value.
To achieve this, we are gonna use rehype-slug.
rehype-slug does the following work automatically.
- Add
ids toh1-h6tags using the inner text as a value - Make sure each
idvalue will be unique
How rehype-slug works
Let me show you how this plugin works. Let's say your mdx file has a content below.
## This is heading 2
### This is heading 3
#### Sample heading
##### Sample headingWhen this is rendered in the browser, it looks like this.
<h2 id="this-is-heading-2">This is heading 2</h2>
<h3 id="this-is-heading-3">This is heading 3</h3>
<h4 id="sample-heading">Sample heading</h4>
<h5 id="sample-heading-1">Sample heading</h5>rehype looks for heading tags, and adds the id attributes with the value of the text content inside.
Also, it generates unique id values even when the heading tags have the same text strings. You can see it by looking at two headings with Sample heading strings inside. When rehype-slug detects the same strings, it adds an additional value to make them unique.
Set up rehype-slug in contentlayer.config.js
To use the rehype plugin, you need to import it in contentlayer.config.js.
import rehypeSlug from "rehype-slug";
export default makeSource({
contentDirPath: "posts", // your own setting
documentTypes: [Post], // your own setting
mdx: {
rehypePlugins: [rehypeSlug],
},
});This is all you need. You simply include rephype-slug in the rehypePlugins array. If you navigate to your blog page, you will see the headings with id attributes.
Step 3. Create a toc component
Now, let's start building the table of contents using Tocbot. First we create a new file toc.jsx, and copy & paste the code below.
import { useEffect } from "react";
import tocbot from "tocbot";
export default function Toc() {
useEffect(() => {
tocbot.init({
tocSelector: ".js-toc", // Select the wrapper of toc
contentSelector: ".js-toc-content", // Select the warpper of contents
headingSelector: "h2, h3", // Choose the heading tags
/* Optional 1.
Enable these if you have a sticky header and adjust the offset value
*/
// headingsOffset: 100,
// scrollSmoothOffset: -100,
/* Optional 2.
Enable this if 'active' class on scroll won't work properly
*/
// hasInnerContainers: true,
});
return () => tocbot.destroy();
}, []);
return (
<div>
<span>Table of Contents</span>
<div className="js-toc"></div>
</div>
);
}This is the basic setup with minimum options.
I'm gonna break these down and explain individually in the next section.
3-1. Initialize tocbot
We initialize tocbot using React useEffect.
After the component is rendered, it creates the table of contents. Once it's done, call destroy method to remove event listeners.
useEffect(() => {
tocbot.init({
tocSelector: ".js-toc", // Select the wrapper of toc
contentSelector: ".js-toc-content", // Select the warpper of contents
headingSelector: "h2, h3", // Choose the heading tags
/* Optional 1.
Enable these if you have a sticky header and adjust the offset value
*/
// headingsOffset: 100,
// scrollSmoothOffset: -100,
/* Optional 2.
Enable this if 'active' class on scroll won't work properly
*/
// hasInnerContainers: true,
});
return () => tocbot.destroy();
}, []);You can pass some options when initializing the tocbot. These 3 options are essential in order to create a ToC.
tocSelector- Where to render the table of contents.contentSelector- Where to grab the headings to build the table of contents.headingSelector- Which headings to grab inside of the contentSelector element.
About the optional settings and other options, check out the official Tocbot page.
3-2. Create the Table of Contents template
We need to create a template to show the table of contents.
import { useEffect } from "react";
import tocbot from "tocbot";
export default function Toc() {
useEffect(() => {
tocbot.init({
tocSelector: ".js-toc",
contentSelector: ".js-toc-content",
headingSelector: "h2, h3",
});
return () => tocbot.destroy();
}, []);
return (
<div>
<span>Table of Contents</span>
<div className="js-toc"></div>
</div>
);
}First, you need to have the container of the generated table of contents. In this code, the div tag with the className of js-toc will be the container because we set tocSelector value to js-toc.
The rest is totally up to you. You can add more tags or some icons if you want.
Step 4. Add a class to the wrapper of blog content in a single page
We need to tell Tocbot where to grab all headings to make the table of contents. In order to do this, we need to add a few lines to a single page (in my case /pages/posts/[slug].js).
If you have a MDX blog with Contentlayer, your [slug].js file should look something like this:
import { useMDXComponent } from "next-contentlayer/hooks";
import { allPosts } from "contentlayer/generated";
const mdxComponents = {
// ...
};
const PostLayout = ({ post }) => {
const MDXContent = useMDXComponent(post.body.code);
return (
<>
{/* Other contents... */}
<MDXContent components={mdxComponents} />
</>
);
};
export default PostLayout;
export async function getStaticProps({ params }) {
// ...
}
export async function getStaticPaths() {
// ...
}<MDXContent /> is where the MDX content will be rendered. So, we add a div tag with js-toc-content class and wrap up <MDXContent /> like this:
return (
<>
<div className="js-toc-content">
<MDXContent components={mdxComponents} />
</div>
</>
);By doing this, Tocbot will look for heading tags inside of js-toc-content and generate the table.
Step 5. Import the toc component into your page
Finally, we are gonna import the toc in your page. There are two ways to do this, depending on where you want to show the table of contents.
1. Use the table of contents inside of .mdx files
If you wanna embed the table of contents inside of your .mdx files, first you need to add it to mdxComponents.
Go to your single page ([slug].js), and add the highlighted lines:
import { useMDXComponent } from "next-contentlayer/hooks";
import { allPosts } from "contentlayer/generated";
import { Toc } from "@/components/toc";
const mdxComponents = {
Toc,
};
const PostLayout = ({ post }) => {
const MDXContent = useMDXComponent(post.body.code);
return (
<>
{/* Other contents... */}
<MDXContent components={mdxComponents} />
</>
);
};
export default PostLayout;
export async function getStaticProps({ params }) {
// ...
}
export async function getStaticPaths() {
// ...
}Now that the <Toc /> component is ready to be used in .mdx files, you can call it like this:
---
title: Sample mdx file
publishedAt: 2023-02-04T20:00:00
---
This is a sample content.
<Toc />
This is a sample content...The cool thing about this method is that you have a control over where to show and whether to show the table of contents on each page.
2. Use the table of contents outside of .mdx files
The common case for this is when you want to show the table of contents in a sidebar. In this case, you can simply import and use it in the file that you actually wanna show it just like other components.
In my blog, I show it in the sidebar which is located in [slug].js file.
import { useMDXComponent } from "next-contentlayer/hooks";
import { allPosts } from "contentlayer/generated";
import { Toc } from "@/components/toc";
const mdxComponents = {
// ...
};
const PostLayout = ({ post }) => {
const MDXContent = useMDXComponent(post.body.code);
return (
<>
<main>
<MDXContent components={mdxComponents} />
</main>
<aside>
<Toc />
</aside>
</>
);
};
export default PostLayout;
export async function getStaticProps({ params }) {
// ...
}
export async function getStaticPaths() {
// ...
}This is it! Now you have successfully implemented the table of contents in your website.
Final thoughts
I originally tried to create the table of contents on my own, but then I realized it would save a lot of time if I used a package. Luckily Tocbot has everything I wanted for my table of contents, so I'm satisfied with this for now.
If you have any questions about this, feel free to send me a message! I'll do my best to help you.