Skip to content

How to Set Up MDX in Next.js

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:

  1. Install @next/mdx dependencies

    npm install @next/mdx @mdx-js/loader
    
  2. Adjust your application’s config in next.config.js

    next.config.js
    const 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 inside pages folder to be turned into its own page.

  3. Create a file in pages/hello-world.mdx and add your MDX content

    pages/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.

pages/hello-world.mdx
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.

components/Post.tsx
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.

pages/hello-world.mdx
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.

components/Post.tsx
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.

next.config.mjs
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:

  1. Install next-mdx-remote package

    npm install next-mdx-remote
    
  2. Run your MDX content through serialize inside a page component’s getStaticProps

    pages/Post.jsx
    import { 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);
    
  3. Register the components you use in Markdown and render it with MDXRemote component

    pages/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.

pages/Post.jsx
// ✂️
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.

components/PostLayout.jsx
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.

pages/Post.jsx
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.

  1. Install mdx-bundler and its dependencies

    npm install mdx-bundler esbuild
    
  2. Run your Makrdown through bundleMDX function inside getStaticProps

    pages/Post.jsx
    import { 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 the bundleMDX 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 in cwd.

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

pages/Post.jsx
// ✂️
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.

components/PostLayout.jsx
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.

pages/Post.jsx
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.

pages/Post.jsx
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.