NextJS, MDX and ToC

January 23, 2023

For one of my NextJS projects, I wanted to create a nice FAQ page. As I planned to have quite a lot of content there, I thought it would be a good idea to have a Table of Contents (TOC) section to make the page easier to navigate, and that's what this post is about.

I will focus on NextJS but a very similar approach should be possible wherever you use MDX or directly remark/rehype.

Technologies used:

  • NextJS 13
  • MDX and @next/mdx for markdown support
  • remark/rehype plugins for generating the Table of Contents

You can find the full source code at https://github.com/OndrejNepozitek/nextjs-mdx-toc and the website is also live at https://nextjs-mdx-toc.netlify.app/.

Table of Contents

  1. Basic project setup
  2. next.config, remark and ES modules
  3. TOC with remark-toc
  4. Heading ids with rehype-slug
  5. Heading links with rehype-autolink-headings
  6. Conclusion

Basic project setup

I created an empty NextJS project with Typescript support based on this guide. Then, I used this guide to add MDX support. According to the guide, you have to:

  1. Install the required packages:

npm install @next/mdx @mdx-js/loader @mdx-js/react

  1. Modify the next.config.js so it looks like this:
next.config.js
Copy

const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// Optionally, add any other Next.js config below
reactStrictMode: true,
}
// Merge MDX config with Next.js config
module.exports = withMDX(nextConfig)

After that, you should be able to have .mdx pages in your pages directory. For example, if you create a example.mdx file and add some markdown inside, you should be able to navigate to <root url>/example and see the markdown converted to HTML.

next.config, remark and ES modules

In order to generate the Table of Contents section, we need to register a remark plugin in the @next/mdx library. remark and rehype are tools that make it possible to transform markdown to HTML with the use of various plugins.

The problem is that there remark and rehype plugins now mostly use ES modules (or ECMAScript modules, ESM) while the next.config.js file uses CommonJS modules. Fortunately, it is relatively easy to switch from CommonJS to ESM. First, start by renaming the next.config.js to next.config.mjs. Next, change the config like this:

next.config.mjs
Copy

import createMDX from "@next/mdx";
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: []
}
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// Optionally, add any other Next.js config below
reactStrictMode: true
}
// Merge MDX config with Next.js config
export default withMDX(nextConfig);

Note how we use import() instead of require() and the module.exports line was changed to export default.

If you now restart the project, you shouldn't see any errors and the website should work exactly the same as previously.

TOC with remark-toc

remark-toc is a remark plugin that can generate the Table of Contents section. It can take a markdown file like the following one:


# Alpha
## Table of contents
## Bravo
### Charlie
## Delta

and transforms the Markdown like this:


# Alpha
## Table of contents
* [Bravo](#bravo)
* [Charlie](#charlie)
* [Delta](#delta)
## Bravo
### Charlie
## Delta

First, install the remark-toc plugin.


npm install remark-toc

Then, register the plugin in @next/mdx in the remarkPlugins config section:

next.config.mjs
Copy

import createMDX from "@next/mdx";
import remarkToc from "remark-toc";
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
remarkToc,
],
rehypePlugins: []
}
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// Optionally, add any other Next.js config below
reactStrictMode: true
}
// Merge MDX config with Next.js config
export default withMDX(nextConfig);

If you restart your project now, you should see the Table of Contents being generated. If it doesn't generate, make sure that you have a heading called Table of contents somewhere in your markdown as that's what the remark-toc plugin looks for when generating the TOC.

Heading ids with rehype-slug

We can now generate the Table of Contents automatically, but if you try clicking on the links, they actually don't work. The reason is the remark-toc plugin only generates the TOC but we also need to add ids to the headings. The rehype-slug plugin can do exactly that.

First, install the plugin:


npm install rehype-slug

Next, add the plugin to the rehypePlugins section of the @next/mdx configuration:

next.config.mjs
Copy

import createMDX from "@next/mdx";
import remarkToc from "remark-toc";
import rehypeSlug from "rehype-slug";
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
remarkToc,
],
rehypePlugins: [
rehypeSlug,
]
}
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// Optionally, add any other Next.js config below
reactStrictMode: true
}
// Merge MDX config with Next.js config
export default withMDX(nextConfig);

Without the plugin, the HTML output would look like this:


<h1>Alpha</h1>
<h2>Table of contents</h2>
<ul>
<li>
<p>
<a href="#bravo">Bravo</a>
</p>
<ul>
<li>
<a href="#charlie">Charlie</a>
</li>
</ul>
</li>
<li>
<p>
<a href="#delta">Delta</a>
</p>
</li>
</ul>
<h2>Bravo</h2>
<h3>Charlie</h3>
<h2>Delta</h2>

With the plugin, the outputs looks like this and the links should be fully functional:


<h1 id="alpha">Alpha</h1>
<h2 id="table-of-contents">Table of contents</h2>
<ul>
<li>
<p>
<a href="#bravo">Bravo</a>
</p>
<ul>
<li>
<a href="#charlie">Charlie</a>
</li>
</ul>
</li>
<li>
<p>
<a href="#delta">Delta</a>
</p>
</li>
</ul>
<h2 id="bravo">Bravo</h2>
<h3 id="charlie">Charlie</h3>
<h2 id="delta">Delta</h2>

The last thing I want to show you is how to add links from headings back to themselves. This features comes in handy if you have a lot of headings on your page and want to share a link to a specific heading or section of the page. The rehype-autolink-headings plugin does exactly that.

First, install the plugin:


npm install rehype-autolink-headings

Next, register the plugin in the @next/mdx configuration:

next.config.mjs
Copy

import createMDX from "@next/mdx";
import remarkToc from "remark-toc";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [
remarkToc,
],
rehypePlugins: [
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behaviour: 'append',
properties: {
ariaHidden: true,
tabIndex: -1,
className: 'hash-link'
}
}
]
]
}
})
/** @type {import('next').NextConfig} */
const nextConfig = {
// Configure pageExtensions to include md and mdx
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// Optionally, add any other Next.js config below
reactStrictMode: true
}
// Merge MDX config with Next.js config
export default withMDX(nextConfig);

Note that I used a slightly different syntax for registering the plugin. That's because each of these remark/rehype plugins comes with some configuration options and this is how you can override the default settings.

If you now restart the website, you won't see any difference. That's because the links are empty by default. You can fix it with a few lines of css:


.hash-link::before {
content: '#';
}

Conclusion

That's it. Next time you want to generate Table of Contents for your markdown content, you should know how.

You can find the full source code at https://github.com/OndrejNepozitek/nextjs-mdx-toc and the website is also live at https://nextjs-mdx-toc.netlify.app/.


Written by Ondřej Nepožitek, who is a software developer and procedural generation enthusiast. In his free time, he usually works on Edgar, his graph-based procedural level generator.

Want to get in touch? See the About for contacts or leave a comment below.