Making a Table of Contents for mdx posts with Next.js
Next.js is a web development framework that supports React components, folder based routing, static page rendering, and server-side requests, to name a few. I recently made my personal website using Next.js for the first time.
MDX is an extension of markdown, a terse markup language for html, but extends support for file imports, exports, and jsx (e.g. for incorporating React components).
After reading this great blog post by Josh Comeau, I decided I would try using mdx with with Next.js to build my personal blog. I then went of a bit of an adventure trying different packages and strategies.
mdx rendering options in Next.js
There are several packages for incorporating mdx files into Next.js including:
- The official
@next/mdx
- Hashicorp's
next-mdx-remote
- Hashicorp's
next-mdx-enhanced
- Kent C Dodd's
mdx-bundler
See this post by Tyler Smith for an overview of the strengths and weaknesses of each approach.
I ended up flip-flopping between @next/mdx
and next-mdx-remote
but ultimately chose @next/mdx
after a long journey that centered around building a Table of Contents like you see in the full-screen version of this page. At one point I even had my code base supporting @next/mdx
and next-mdx-remote
for different parts of my website, which shows they do not conflict.
My summary of the two
@next/mdx | next-mdx-remote | |
---|---|---|
Export content as React components | ✓ | |
Offical support from Next.js (Vercel) | ✓ | |
.mdx can be rendered as pages | ✓ | |
Supports import and export | ✓ | |
Automatic refresh on save | ✓ | |
Remote (serialised) markdown, e.g on servers | ✓ | |
Built-in frontmatter support | ✓ |
Some useful resources
If you want to make a blog, there are some good existing resources including this comprehensive article by Elijah Agbonze.
Another honorable mention is an mdx extension of Vercel's official blog starter example on github by John Polacek, but it is a bit out of date.
Making the Table of Contents
In summary, I use API requests to the server which parses the serialised front matter, extracting the titles similar to Josh Comeau's solution, but using @next/mdx
to render mdx, which allows us to keep some useful features of .mdx files.
The MDXProvider
wraps the page in _app.js
to create a context that allows the mdx files access to the supplied components. The wrapper
component is a special component that will wrap the mdx content.
// _app.js
import './markdown-wrapper.js'
const MDXComponents = {
// ...
wrapper: ({components, ...props}) => <MarkdownWrapper components={components}{...props} />,
// ...
};
function MyApp({ Component, pageProps }) {
return (
<>
<MDXProvider components={MDXComponents}>
<Component {...pageProps} />
</MDXProvider>
</>
)
}
export default MyApp
In markdown-wrapper.js
I create a state for headings, make an API request to retrieve headings, and then add it to the page.
// markdown-wrapper.js
import classes from './markdown-wrapper.module.css'
import React, { useEffect, useState } from 'react';
import {useRouter} from 'next/router';
import TableOfContents from '../../components/posts/table-of-contents';
const MarkdownWrapper = (props) => {
const [headings, setHeadings] = useState('');
const router = useRouter()
const loadContents = async () => {
const slug = router.asPath.replace(/\/(\w+)\//, '')
const postsFolder = router.xxasPath.match(/\w+/)[0]
const res = await fetch(`/api/post-toc?slug=${slug}&postsFolder=${postsFolder}`);
const data = await res.json();
setHeadings(data)
};
useEffect(() => {
loadContents()
}, [])
return (
<>
<div className={classes.container}>
{headings && <TableOfContents headings={headings}/> }
<div id="introduction"></div>
<div className={classes.postContainer} {...props}></div>
</div>
</>
);
}
To make the url link to ech page sections work, I assign a lower case id to each heading based on the title name and then update the component styles
// _app.js
import './markdown-wrapper.js'
const Heading2 = ({ children }) => {
const idText = children.replace(/ /g, "_").toLowerCase();
return <h2 id={idText} className={classes.heading2}>{children}</h2>;
};
const MDXComponents = {
// ...
h2: Heading2,
wrapper: ({components, ...props}) => <MarkdownWrapper components={components}{...props} />,
// ...
};
//...
Note that I manually insert the introduction in markdown-wrapper.js
which I then hid in the styles module.
Conclusion
To be honest, this whole activity, was a bit of a learning excercise, which was not efficient from the perspective of making a blog site. But I am looking forward to tinkering with other aspects of the page as I realise just how flexible and powerful a Next.js + mdx approach to blogging is.