DEV Community

Tyler Smith
Tyler Smith

Posted on • Updated on

Comparison of MDX integration strategies with Next.js

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:

  1. The official way, with @next/mdx
  2. Hashicorp's next-mdx-enhanced
  3. Hashicorp's next-mdx-remote
  4. 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);
Enter fullscreen mode Exit fullscreen mode

@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>.
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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} />;
Enter fullscreen mode Exit fullscreen mode

@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";

Enter fullscreen mode Exit fullscreen mode

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 */);
Enter fullscreen mode Exit fullscreen mode

next-mdx-enhanced Pros:

The next-mdx-enhanced plugin supports frontmatter.

--------
# pages/index.mdx

layout: PageLayout
title: "Homepage"
--------

This is my homepage.
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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 } };
}
Enter fullscreen mode Exit fullscreen mode

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 />
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (11)

Collapse
sanketss84 profile image
Sanket Sonvane

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.

Collapse
tylerlwsmith profile image
Tyler Smith Author

Thanks for reading. I'm glad you got some value out of it!

Collapse
sanketss84 profile image
Sanket Sonvane • Edited on

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 ?

Thread Thread
tylerlwsmith profile image
Tyler Smith Author

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.

Thread Thread
sanketss84 profile image
Sanket Sonvane

Thanks for sharing your thoughts.

Collapse
camsloanftc profile image
Cam Sloan

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

Collapse
tylerlwsmith profile image
Tyler Smith Author

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.

Collapse
tikitariki profile image
Tarik Hamilton

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."

Collapse
tylerlwsmith profile image
Tyler Smith Author

Yeah: I may have wasted a whole day trying to figure out what was going on with that 😂

Collapse
rajuashok profile image
Ashok Raju

Thank you for this very helpful guide! Made deciding between these quite easy.

Collapse
tylerlwsmith profile image
Tyler Smith Author

Awesome! What did you end up choosing?