Setting up MDX with Next.js

Separating contents from code.
3 min read·Oct 19, 2022

For a personal website like this, I think setting up a comfortable way to write text-based contents is important.

And when it comes to writing text on the web, markdown gives you far better experience than writing an article in html tags.

While writing contents in markdown is already good enough for most cases, MDX offers something even better on top of what markdown is already capable of.

MDX allows you to use JSX in your markdown content. You can import components, such as interactive charts or alerts, and embed them within your content. This makes writing long-form content with components a blast. 🚀

Pay attention to this part of the statement again:
"You can import components, such as interactive charts ..."

I mean.. how cool is that?


So I think it's always better to go one step further and set up an MDX workflow if possible, rather than stopping at plain markdown setup if you're planning to write text-based contents for your website.

So how do we set this up in Next.js app?

Basic idea is very similar to that of Next.js official tutorial, in which they show us how to build a blog app with Next.js.

So the steps of processing MDX are something like this:

  • Read the content of .mdx file in getStaticProps()
  • Serialize the loaded mdx content and return as one of the props in getStaticProps()
  • Process the props passed from getStaticProps() in the actual React Component of the page.

The most non-trivial part of this process is to serialize and deserialize the mdx content.

Fortunately, a wonderful library called, next-mdx-remote handles this effortlessly.

Let's examine the example code below:

import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";

import Test from "../components/test";

const components = { Test };

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  );
}

export async function getStaticProps() {
  // MDX text - can be from a local file, database, anywhere
  const source = "Some **mdx** text, with a component <Test />";
  const mdxSource = await serialize(source);
  return { props: { source: mdxSource } };
}

If we look at getStaticProps() first—the place where the serialization of MDX should occur—we can identify it's handled by serialize() function from next-mdx-remote/serialize, and the serialized MDX source is returned as one of the props as source.

Next, in the actual React Component of the page <TestPage />, we see that it takes the serialized MDX content passed by getStaticProps() and pass it down to MDXRemote component from next-mdx-remote.

And that's pretty much it.

The original MDX source Some **mdx** text, with a component <Test /> will be correctly transformed to react elements inside <MDXRemote /> and eventually be rendered as DOM nodes in the browser.

One caveat when using a custom react component like <Test /> above with this approach is that you cannot use import/export statements in your MDX file, but rather you must provide your custom component to <MDXRemote /> as a prop:

import { MDXRemote } from "next-mdx-remote";

import Test from "../components/test";

const components = { Test };

export default function TestPage({ source }) {
  return (
    <div className="wrapper">
      <MDXRemote {...source} components={components} />
    </div>
  );
}

This is because next-mdx-remote allows you to pull your MDX source not only from your local directory but also from any remote location where contents are saved—if the content is coming from some remote storage, not from your project's sub-directory, a path-based import/export would not really make sense. (see: import/export)

One more thing before wrapping up.

If you use <MDXProvider /> in your project already, you can also pass your custom component along with your other mdx components to it instead:

import { MDXProvider } from "@mdx-js/react";
import Test from "../components/test";

const components = {
  img: ResponsiveImage,
  h1: Heading.H1,
  h2: Heading.H2,
  p: Text,
  pre: Pre,
  code: InlineCode,
  Test, // this doesn't have to be an MDX component (https://mdxjs.com/table-of-components/#components)
};

export default function MyApp({ Component, pageProps }) {
  return (
    <MDXProvider components={components}>
      <Component {...pageProps} />
    </MDXProvider>
  );
}

Useful references:

Back to list