MDX in Next.js
When it comes to using MDX in the context of a Next.js app, there are quite a few options, including:
- @next/mdx, the official package.
- mdx-bundler by Kent C. Dodds.
- next-mdx-remote by Hashicorp.
I've experimented with all of these options (and others!), and I've found that the best option for me is next-mdx-remote
.
The official @next/mdx
package is the simplest option to get started with, but we sacrifice a lot of flexibility. It's a bit too prescriptive for me.
Kent's mdx-bundler
, on the other hand, feels a little too robust. It includes its own bundler, meaning that our application now has two separate bundlers that have to co-operate. Things like import aliases need to be configured separately; I ran into several issues when I tried to use mdx-bundler
on joshwcomeau.com.
Hashicorp's next-mdx-remote
is in the Goldilocks zone for me. It's incredibly powerful and flexible. It doesn't include its own bundler, so there's no extra maintenance or compatibility issues. Of the 3 options, I think it has the clearest mental model, in terms of how it works.
Also, next-mdx-remote
was recently updated to work with React Server Components, and honestly, it got way more user-friendly in the process. next-mdx-remote
is easier to use than ever.
Now, as always, there are some trade-offs. The biggest downside with next-mdx-remote
is that we can't import directly inside our MDX files. But this isn't a huge deal, as you'll soon learn.
Alright, let's learn how to use next-mdx-remote
!
The big idea
Here's how it works — we load up our MDX and feed it into an <MDXRemote />
component:
import { MDXRemote } from 'next-mdx-remote/rsc';
export default function BlogPost() { const content = ```# Hello world!
This is the content of an MDX file.
- Yes- it- is ```;
return ( <MDXRemote source={content} /> );}
The <MDXRemote>
component will take this raw content
string and transform it into a bunch of React elements. In this particular case, it'll be as if we had returned the following JSX:
return ( <> <h1>Hello world!</h1> <p>This is the content of an MDX file.</p> <ul> <li>Yes</li> <li>it</li> <li>is</li> </ul> </>)
next-mdx-remote
doesn't care where the MDX comes from. In the example above, it's a hardcoded string, but in a more realistic scenario, we'd load it from somewhere, like a database, a CMS, or the local file system.
For example, using the Node fs
(File System) module, we could do something like this:
import path from 'path';import fs from 'fs/promises';import { MDXRemote } from 'next-mdx-remote/rsc';
export default async function BlogPost({ params }) { // Read the content of a locally-stored file as a string: const content = await fs.readFile( path.join(process.cwd(), `/content/${params.slug}.mdx`), 'utf8' );
return ( <MDXRemote source={content} /> );}
This is a Server Component, meaning that this code runs exclusively on the server. We look up the .mdx
file associated with the provided slug
, read all of its content, and pass it into <MDXRemote>
to be rendered.
Custom components
The thing that makes MDX so special is the ability to render custom components. We're not limited to the handful of built-in HTML tags!
Consider this code:
import Link from 'next/link';import { MDXRemote } from 'next-mdx-remote/rsc';
import ContentImage from '@/components/ContentImage';
export default async function BlogPost() { const content = `If you run into any problems, you can[contact us](/contact).
<img alt="Mailbox clipart" src="/img/mailbox.svg"/>`;
return ( <MDXRemote source={content} components={{ a: Link img: ContentImage, }} /> );}
If this was a typical Markdown document, it would get compiled into the following HTML:
<p> If you run into any problems, you can <a href="/contact">contact us</a>.</p>
<img alt="Mailbox clipart" src="/img/mailbox.svg"/>
In MDX, however, we don't compile to HTML, we compile to JSX. And we're able to “remap” Markdown tags to custom React components.
And so, here's the JSX that gets compiled:
<p> If you run into any problems, you can <Link href="/contact">contact us</Link>.</p>
<ContentImage alt="Mailbox clipart" src="/img/mailbox.svg"/>
Pretty cool right? We can “redefine” any of the built-in HTML tags, to render our own versions instead.
Additionally, we can also specify brand-new tags which can be used in our MDX:
import { MDXRemote } from 'next-mdx-remote/rsc'
import FlexDemo from '@/components/FlexDemo';import SocialShareWidget from '@/components/SocialShareWidget';
export default async function BlogPost() { const content = `# Intro to Flexbox
Play around with this demo to get a sense of the Flexbox algorithm:
<FlexDemo />
If you enjoyed this blog post, let your friends know:
<SocialShareWidget /> `;
return ( <MDXRemote source={content} components={{ FlexDemo, SocialShareWidget, }} /> );}
This is the magic of MDX. We can create custom components like <FlexDemo>
and embed them in our MDX documents. We can create as many custom components as we want.