Skip to content

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:

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.