How to Set Up MDX in Next.js
While creating a blog, I wanted to add MDX to my project to use components inside Markdown. Doing a quick search, I found out many options of how to do it.
Apparently to add MDX in Next.js, you have at least 3 options:
So which one do you pick?
In this post you will see how to set up each option and learn about their pros and cons.
Alternatively, you can check out how to build a blog using Contentlayer, which comes with MDX out of box.
MDX Setup With @next/mdx
The @next/mdx package is the official way of how to implement MDX in your Next.js project.
Although, compared to other packages, it feels a bit awkward to use and has a lackluster documentation, it gets the job done with little effort.
Here’s how to set up MDX with @next/mdx
:
-
Install
@next/mdx
dependenciesnpm install @next/mdx @mdx-js/loader
-
Adjust your application’s config in
next.config.js
next.config.jsconst withMDX = require('@next/mdx')({ extension: /\.mdx?$/, }) module.exports = withMDX({ pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], })
Setting up
pageExtensions
will make any.md
or.mdx
file insidepages
folder to be turned into its own page. -
Create a file in
pages/hello-world.mdx
and add your MDX contentpages/hello-world.mdx# Hello World This is my first post. import Hello from '../components/Hello.tsx'; <Hello />
You can import components anywhere in your Markdown.
You will see your content rendered when you open the
/hello-world
route.
Using a Common Layout Component WIth @next/mdx
Something you might want to do is use a shared layout component for all MDX pages.
To achieve that with @next/mdx
, you need to export a default function from the .mdx
file that returns the wrapper component.
import Hello from '../components/Hello.tsx';
import Post from '../components/Post.tsx';
# Hello World
This is my first post.
<Hello />
export default ({ children }) => <Post content={children} />;
Now you can control the layout of your Markdown content inside Post
component.
export default function Post({ content }) {
return (
<article>
<section>{content}</section>
</article>
);
}
Using Frontmatter With @next/mdx
Unfortunately @next/mdx
doesn’t support frontmatter. But, you can specify extra data with export
.
import Hello from '../components/Hello.tsx';
import Post from '../components/Post.tsx';
export const data = {
author: 'John Doe',
date: '2022-08-18',
};
# Hello World
This is my first post.
<Hello />
export default ({ children }) => <Post content={children} data={data} />;
You can then access the extra data through props.
export default function Post({ content, data }) {
return (
<article>
Written by {data.author} on {data.date}
<section>{content}</section>
</article>
);
}
Remark and Rehype Plugins in @next/mdx
Adding remark and rehype plugins is done inside next.config.js
. Since remark and rehype plugins usually use ECMAScript modules, you will most like need to use .mjs
file extension for your config file.
Rename your config file to next.config.mjs
and adjust it for ESM support, which is needed for remark and rehype plugins.
import mdx from '@next/mdx';
import smartypants from 'remark-smartypants';
import rehypePrism from 'rehype-prism-plus';
const withMDX = mdx({
options: {
remarkPlugins: [smartypants],
rehypePlugins: [rehypePrism],
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
};
export default withMDX({
...nextConfig,
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});
@next/mdx Pros
- Allows creating pages directly from Markdown files
- Importing components works effortlessly
@next/mdx Cons
- Can’t render remotely stored MDX, just local files
- Uses
export
as a workaround to add a shared layout and frontmatter
MDX Setup With next-mdx-remote
The next-mdx-remote doesn’t require you to do component imports inside MDX. Instead, you have to register a list of the components you will use throughout your Markdown. You can then simply embed the components in your content.
To set up MDX with next-mdx-remote
, follow these steps:
-
Install
next-mdx-remote
packagenpm install next-mdx-remote
-
Run your MDX content through
serialize
inside a page component’sgetStaticProps
pages/Post.jsximport { serialize } from 'next-mdx-remote/serialize'; import { MDXRemote } from 'next-mdx-remote'; import Hello from '../components/Hello'; export async function getStaticProps() { const mdx = `# Hello World Here's a component used inside Markdown: <Hello />`; const mdxSource = await serialize(mdx); return { props: { source: mdxSource, }, }; }
The
serialize
function expects a string, so your content can come from anywhere.For example, you can read a file and turn the buffer into a string and use that as a source.
import fs from 'fs'; import { serialize } from 'next-mdx-remote/serialize'; const source = (await fs.promises.readFile('content/hello-world.mdx')).toString(); const mdxSource = await serialize(source);
-
Register the components you use in Markdown and render it with
MDXRemote
componentpages/Post.jsx// ✂️ import { MDXRemote } from 'next-mdx-remote'; import Hello from '../components/Hello'; // ✂️ const components = { Hello }; export default function PostPage({ source }) { return <MDXRemote {...source} components={components} /> }
Sharing a Layout Component With next-mdx-remote
To use a shared layout for all pages that render MDX, you can wrap MDXRemote
component inside your own component.
// ✂️
import { MDXRemote } from 'next-mdx-remote';
import Hello from '../components/Hello';
import PostLayout from '../components/PostLayout';
// ✂️
const components = { Hello };
export default function PostPage({ source }) {
return (
<PostLayout>
<MDXRemote {...source} components={components} />
</PostLayout>
);
}
Now you can style and format the content however you want from your layout component.
export default function PostLayout({ children }) {
return (
<article>
<section>{children}</section>
</article>
);
}
Using Frontmatter With next-mdx-remote
With next-mdx-remote
, you can easily extract frontmatter from your Markdown. All you have to do, is pass the serialize
function a second argument of an object containing parseFrontmatter: true
.
import { serialize } from 'next-mdx-remote/serialize';
export async function getStaticProps() {
const mdx = `---
author: John Doe
date: 2022-08-18
---
# Hello World
Here's a component used inside Markdown:
<Hello />`;
const mdxSource = await serialize(mdx, {
parseFrontmatter: true,
});
return {
props: {
source: JSON.parse(JSON.stringify(mdxSource)),
},
};
}
Since Next.js expects getStaticProps
to return data of simple values (numbers, strings, booleans), you may face the following error if your frontmatter contains a date:
Reason: `object` (“[object Date]”) cannot be serialized as JSON. Please only return JSON serializable data types.
That’s why you need to run the data through JSON.parse(JSON.stringify())
and turn the date object into a string.
Remark and Rehype Plugins in next-mdx-remote
To use remark and rehype with next-mdx-remote
, you need to pass a config object to serialize
with an mdxOptions
object.
import { serialize } from 'next-mdx-remote/serialize';
import smartypants from 'remark-smartypants';
import rehypePrism from 'rehype-prism-plus';
export async function getStaticProps() {
const source = `# Hello World`
const mdxSource = await serialize(source, {
mdxOptions: {
remarkPlugins: [smartypants],
rehypePlugins: [rehypePrism],
},
});
return {
props: {
source: mdxSource,
},
};
}
next-mdx-remote Pros
- Can freely use the registered components without writing imports in Markdown
- MDX content can come from anywhere — local filesystem, database etc.
next-mdx-remote Cons
- Can’t create pages from Markdown files like
@next/mdx
MDX Setup With mdx-bundler
Unlike previous options, mdx-bundler is framework agnostic. It means that it’s not built just for use in Next.js, but also other frameworks.
Another difference is that mdx-bundler
is not just compiler, but also a bundler. It is capable of resolving the imports inside your Markdown. You don’t need to specify a list of components that you will use like you have to with next-mdx-remote
.
Here’s how to set up mdx-bundler
.
-
Install
mdx-bundler
and its dependenciesnpm install mdx-bundler esbuild
-
Run your Makrdown through
bundleMDX
function insidegetStaticProps
pages/Post.jsximport { bundleMDX } from 'mdx-bundler'; export async function getStaticProps() { const mdx = `# Hello World Here's a component used inside Markdown: import Hello from './components/Hello'; <Hello />`; const result = await bundleMDX({ source: mdx, cwd: process.cwd(), }); return { props: { code: result.code, }, }; }
Similarly to
next-mdx-remote
, you can compile Markdown from anywhere, as long as you pass thebundleMDX
function a Markdown string.The
cwd
property specifies the directory from which you want to do imports inside your Markdown. So all your imports should be relative to what you specify incwd
. -
Render the bundled code with
getMDXComponent
pages/Post.jsx// ✂️ import * as React from 'react'; import { getMDXComponent } from 'mdx-bundler/client'; // ✂️ export default function PostPage({ code }) { const PostContent = React.useMemo(() => getMDXComponent(code), [code]); return <PostContent /> }
Sharing a Layout Component With mdx-bundler
To re-use a common layout component with mdx-bundler
, just pass the bundled code to it.
// ✂️
import Post from '../components/PostLayout';
// ✂️
export default function PostPage({ code }) {
return <PostLayout code={code} />;
}
And now render the code
with getMDXComponent
inside your layout component.
import * as React from 'react';
import { getMDXComponent } from 'mdx-bundler/client';
export default function PostLayout({ code }) {
const PostContent = React.useMemo(() => getMDXComponent(code), [code]);
return (
<article>
<section>
<PostContent />
</section>
</article>
);
}
Using Frontmatter With mdx-bundler
Accessing frontmatter data using mdx-bundler
requires no extra effort. Frontmatter is automatically extracted when you call bundleMDX
function.
import * as React from 'react';
import { bundleMDX } from 'mdx-bundler';
import { getMDXComponent } from 'mdx-bundler/client';
export async function getStaticProps() {
const mdx = `---
author: John Doe
date: 2022-08-18
---
# Hello World
Here's a component used inside Markdown:
import Hello from './components/Hello';
<Hello />`;
const result = await bundleMDX({
source: mdx,
cwd: process.cwd(),
});
return {
props: {
code: result.code,
frontmatter: JSON.parse(JSON.stringify(result.frontmatter)),
},
};
}
export default function PostPage({ code, frontmatter }) {
const PostContent = React.useMemo(() => getMDXComponent(code), [code]);
return (
<article>
Written by {frontmatter.author} on {frontmatter.date}
<section>
<PostContent />
</section>
</article>
);
}
Remark and Rehype Plugins in mdx-bundler
To use remark and rehype plugins with mdx-bundler
, you will need to pass the mdxOptions
function to bundleMDX
. Through the options
argument that you pass to mdxOptions
, you can override default remarkPlugins
and rehypePlugins
.
import { bundleMDX } from 'mdx-bundler';
import smartypants from 'remark-smartypants';
import rehypePrism from 'rehype-prism-plus';
export async function getStaticProps() {
const mdx = `# Hello World`;
const result = await bundleMDX({
source: mdxSource,
mdxOptions(options) {
options.remarkPlugins = [...(options.remarkPlugins ?? []), [smartypants]];
options.rehypePlugins = [...(options.rehypePlugins ?? []), [rehypePrism]];
return options;
},
});
return {
props: {
code: result.code,
},
};
}
mdx-bundler Pros
- Can bundle MDX from anywhere given a string or a file
- Rich in features, see documentation
mdx-bundler Cons
- Similarly to
next-mdx-remote
, takes work to set up
Summary
Now that you have seen how to set up 3 different packages to add MDX to your Next.js project, make a choice and just pick one. They all get the job done, but cover different needs.
The @next/mdx
offers simplicity and page creation out of box, but might feel a bit awkward to use. While next-mdx-remote
and mdx-bundler
come with more features, especially the latter.
I’ve been using mdx-bundler
in my projects lately, since it comes with Contentlayer, which I often use.