I wanted to use MDX files on my Next.js site, but I discovered that there are a lot of ways to implement MDX with Next. Since I spent a whole day reviewing these options, I wanted to write down what I learned in case I ever need to revisit this decision in the future.
I learned from Joshua Comeau's blog post that there are four popular packages for accomplishing this. Here are the options that Joshua presents:
- The official way, with @next/mdx
- Hashicorp's next-mdx-enhanced
- Hashicorp's next-mdx-remote
- Kent C Dodds' mdx-bundler
Vercel's @next/mdx
The @next/mdx package is Vercel's (the organization behind Next.js) official way of using MDX with Next.js. To add it to your project, you install it then wrap your next.config.js
object in in a withMDX
function and tell next to accept MDX page extensions. Next will then be able to process MDX as a page or as a React component.
// next.config.js
const withMDX = require("@next/mdx")({
extension: /\.mdx?$/,
});
const nextConfig = {
/** Tell Next that it should render MDX files as page if they're in the page directory. */
pageExtensions: ["js", "jsx", "mdx"],
};
/** Wrap the config so it can add all of the necessary configuration to process MDX iles. */
module.exports = withMDX(nextConfig);
@next/mdx Pros:
You can import ad hoc components in your MDX files without issue. In some other packages, you can't do imports within the MDX file itself.
// content/some-file.mdx
import Button from "../components/Button";
# This is my markdown file
I can import components and use them like this <Button>button</Button>.
You can import MDX files and use them like React components with very little friction.
// pages/index.js
import { PageLayout } from "../components/PageLayout.mdx";
import HomepageMDX, { author } from "../content/Homepage.mdx";
export default function Homepage() {
return (
<PageLayout author={author}>
{/** We can use MDX files as components */}
<HomepageMDX />
</PageLayout>
);
}
You can also put MDX files directly in your pages/
directory. If you do this, you'll likely want to wrap the page in some kind of layout. You can do that by exporting a default function that accepts your MDX content as children and other exported values as props.
// pages/index.mdx
import PageLayout from "../components/PageLayout";
# Homepage
This is my homepage.
// You can wrap the page in a component by making the default export a function.
export default ({ children }) => <PageLayout author={author} children={children} />;
@next/mdx Cons:
Unfortunately, @next/mdx
doesn't support frontmatter. You can export data with standard ES6 export
syntax, but feels awkward.
// pages/exports-are-awkward.mdx
import PageLayout from "../components/PageLayout";
export const author="Tyler Smith";
export const date="2021-07-14"
# Exports are awkward
Just look at them.
export default ({ author, date, children }) => (
<PageLayout author={author} date={date} children={children} />
);
// If you're importing MDX as a component to another file, it would look like:
// import Content, { author, date } from "../content/exports-are-awkward.mdx";
The Emotion.js Babel loader doesn't seem to work with @next/mdx
. When using the Babel configuration from the official Next.js Emotion example, you get an error that says SyntaxError: importSource cannot be set when runtime is classic
. I believe that this problem is from @next/mdx
using an older version of React's runtime than Emotion is using and that it is not an issue with Emotion itself. There is an open issue discussing this on the MDX GitHub.
At the time of writing this (August 2021), TypeScript support is not implemented on the underlying @mdx-js/loader
package. The docs say that TypeScript is supported, but the link to @mdx-js
docs for @mdx-js v2.0.0-next.9 instead of the current 1.6.22 version. However, it will be supported in the version 2.0 release.
The official @next/mdx docs are lacking. They don't even show how to wrap your page in a component; I found that in a Lee Robinson post on the Next.js blog.
Finally, all the MDX must be from MDX files within the project: you can't render MDX that is stored in a database.
Hashicorp's next-mdx-enhanced
First and foremost: this project is no longer maintained and doesn't work with Next.js 11 (GitHub issue). The next-mdx-enhanced package is similar to @next/mdx
but with more features.
Like @next/mdx
, you add next-mdx-enhanced
to the project by exporting a function in the project's next.config.js
file.
// next.config.js
const withMdxEnhanced = require("next-mdx-enhanced");
module.exports = withMdxEnhanced({
layoutPath: "layouts",
defaultLayout: true,
fileExtensions: ["mdx"],
})(/* your normal nextjs config */);
next-mdx-enhanced Pros:
The next-mdx-enhanced
plugin supports frontmatter.
---
# pages/index.mdx
layout: PageLayout
title: "Homepage"
---
This is my homepage.
It also supports defining a layout within the frontmatter as seen in the example above. By default, these layouts live in a layouts/
directory in the root of your project, and accept the content and front matter as arguments.
export default function PageLayout({ children, frontMatter }) {
return (
<div>
<h1>{frontMatter.title}</h1>
{children}
</div>
);
}
next-mdx-enhanced Cons:
The project is no longer maintained and won't work on Next.js 11. When I tried setting it up, every page I navigated to on the site just said Internal Server Error
in plain text.
If you had a non-trivial amount of MDX files when using next-mdx-enhanced
, the memory consumption could become massive. The readme for next-mdx-remote
(also by Hashicorp) says that with a couple hundred pages, developing the site could consume 8 gigs of ram and take 25 minutes to build.
Like @next/mdx
, all the MDX for next-mdx-enhanced
must be from MDX files within the project: you can't render MDX that is stored in a database.
You also cannot import JavaScript modules from within MDX files when using next-mdx-enhanced
: permitted components must be specified in the configuration.
Hashicorp's next-mdx-remote
The next-mdx-remote package by Hashicorp was created to deal with problems that arose from their next-mdx-enhanced
package.
Unlike the previous plugins, your content can come from anywhere: markdown files, a database, an API, etc. That's because the markdown is loaded through getServerProps
or getStaticProps
.
The package does not require global configuration like @next/mdx
or next-mdx-enhanced
; instead, you load the serialize
function and <MDXRemote />
component on any page you want to use it in. Here is a full example from the project's README.md:
import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";
import matter from "gray-matter";
import Test from "../components/test";
const components = { Test };
export default function TestPage({ source, frontMatter }) {
return (
<div className="wrapper">
<h1>{frontMatter.title}</h1>
<MDXRemote {...source} components={components} />
</div>
);
}
export async function getStaticProps() {
// MDX text - can be from a local file, database, anywhere
const source = `---
title: Test
---
Some **mdx** text, with a component <Test name={title}/>
`;
const { content, data } = matter(source);
const mdxSource = await serialize(content, { scope: data });
return { props: { source: mdxSource, frontMatter: data } };
}
next-mdx-remote Pros:
Perhaps most substantially, next-mdx-remote
does not suffer from the intense performance issues that next-mdx-enhanced
suffered from, meaning it's unlikely to run into the 25 minute builds mentioned above.
The next-mdx-remote
package also supports frontmatter and it supports remote data loading. It does not require changes to the project's next.config.js
configuration file. Passing in components to the <MDXRemote />
component is also trivial.
next-mdx-remote Cons:
There is more "glue code" to write with next-mdx-remote
than @next/mdx
or next-mdx-enhanced
because you aren't leveraging file-based routing.
You also lose out on the nice next-mdx-enhanced
layouts feature, though it's likely possible to implement a simple version of that feature yourself.
MDX Bundler
MDX Bundler differs from all of the other packages listed above in that it is Framework agnostic, meaning you can use it outside of Next.js. Like next-mdx-remote
, no changes to the next.config.js
file are necessary.
MDX Bundler Pros:
Perhaps the biggest pro is that MDX Bundler can support Remix/Gatsby/Next/CRA/etc.
MDX Bundler supports frontmatter, and it also supports imports within the MDX files. Similar to next-mdx-remote
, MDX Bundler can use content from anywhere (the filesystem, database, API, etc).
Here is an example of dynamic imports from the docs:
---
title: Example Post
published: 2021-02-13
description: This is some description
---
# Wahoo
import Demo from "./demo";
Here's a **neat** demo:
<Demo />
This is possible because MDX Bundler is a bundler and not just a compiler: bundling its dependencies using esbuild. esbuild is also extremely fast compared to other build tools, and it is capable of building and bundling on-the-fly.
MDX Bundler Cons:
MDX Bundler requires esbuild as a peer dependency. This isn't necessarily bad, but it feels odd compared to the other options. You may need to set esbuild settings, which is more configuration that you need to learn and understand.
While I haven't tested it personally, the dynamic imports in MDX files feel like they could be a security implication if loading data from untrusted sources. There are likely ways to mitigate this though.
Final thoughts
Which MDX package did I use in my project? I went with @next/mdx
because it met my needs with minimal overhead. At some point I may migrate to next-mdx-remote
, but only when it becomes painful to continue doing what I'm doing.
Regardless of if you use @next/mdx
, next-mdx-remote
or MDX Bundler, you can leverage the power of MDX to add cool features to your content.
Top comments (11)
Thanks for this, I also ended up choosing @next/mdx as I like minimal overhead and all my needs are being met. With Next 12 this seems to be a hassle free option for now.
Thanks for reading. I'm glad you got some value out of it!
What strategy do you use for listing blog posts and site wide search ?
Option 1
I am thinking of going with creating a json file called posts.json where I will add paths manually for all MDX posts rather than doing file path scanning jugglery. This json object will have slug, path, tags, create date, author. I shall also use this along with fuze.js to filter the results.
Option 2
There is a library called glob which scans path for *.MDX files as well
For search I am thinking of using fuze.js
I have heard (not tried at all) about algolia instant search and elastic search but i am wondering do I really need external indexes ?
What are your thoughts ?
I was using markdown for a few pages, so I haven't implemented this in Next.js. I'd personally lean towards option 2 and be very careful to make sure that no one can execute a directory traversal attack.
In the past on non-Next.js sites, I've actually ingested the markdown content into a SQLite DB on application bootup and used that for searching with LIKE queries. It wasn't typo tolerant, but it was good enough for my needs. I had less than 100 posts though, and if I had upwards of 1000 it would probably slow down the application bootup.
Thanks for sharing your thoughts.
Thanks for writing this, Tyler. You're right that the current documentation is pretty unclear and it's a tough landscape to navigate. Thank you for clarifying a lot of it for us.
Quick question for you about your mdx setup: do you have a route in your site that acts like a blog homepage, which includes links to all your other mdx posts?
I am curious how to go about setting that up in a somewhat automated way with @next/mdx. Ex: a landing page that lists all the mdx posts sorted by date, then links out to each one.
Thanks!
Cam
Hey Cam,
Thanks for reading! I don't have anything like that: on my site, the only mdx pages are About, FAQ and Contact.
However, Lee Robinson recommended in a tweet that
npx create-next-app -e blog
is a great way to get started with a Next-powered blog. Lee is on the Vercel team so his advice tends to be pretty solid.I'd run that command in a new directory and see how that's structured: I feel like that may have everything you need to get started.
Dude, thank you for confirming this...
"At the time of writing this (August 2021), TypeScript support is not implemented on the underlying @mdx-js/loader package. The docs say that TypeScript is supported, but the link to @mdx-js docs for @mdx-js v2.0.0-next.9 instead of the current 1.6.22 version. However, it will be supported in the version 2.0 release."
Yeah: I may have wasted a whole day trying to figure out what was going on with that ๐
Thank you for this very helpful guide! Made deciding between these quite easy.
Awesome! What did you end up choosing?