Chris.luChris.lu header image, displaying an 80s style landscape and sunset

Next.js MDX support

In this part of the tutorial, we are finally going to add MDX (version 3) support to our project by using next/mdx

Why use markdown (MDX)?

As a developer, using Markdown to format content makes sense, as most of us probably already know Markdown because we have been using it when formatting our project READMEs, or when opening Issues or participating in Discussions on GitHub, or when formatting questions and answers on Stack Overflow, or even in messages on a Discord server

This is why I chose to use MDX (markdown + JSX) for this "static Next.js blog" project

Adding MDX support to our Next.js project

To add MDX support to the Next.js blog, I will use @next/mdx, which is an MDX package by the same team that is behind Next.js, but other alternatives work well too, I listed some of them in the @next/mdx alternatives chapter in my MDX post

You can also create your own package, in which case I recommend you start by reading the Deep Dive: How do you transform markdown into HTML? section of the Next.js "Markdown and MDX" documentation

Note

If you want to get more background information about MDX, I recommend checking out my MDX post

@next/mdx is stable, and I had no significant problems using it for this project, there are however a series of open Issues on GitHub, I recommend having a brief look at the @next/mdx issues search on GitHub to have an idea of what might not work or just have a look at the list when you have the feeling something is not working as it should

@next/mdx installation

First, we will add the @next/mdx package and the MDX types to our Next.js project, the @next/mdx package will add support for MDX files to our Next.js project, to install the package execute the following command in your VSCode terminal:

npm i @next/mdx @types/mdx --save-exact

MDX support setup

First, we need to create a new file called mdx-components.tsx in the root of our project (or the ./src folder if you have configured your project to use the ./src folder)

Warning

Adding the file is very important, I see a lot of posts of developers struggling with the first steps of getting MDX support to work in their Next.js project, and 90% of the cases are because they have not added the mdx-components.tsx file, without mdx-components.tsx in Next.js 13 you will get an error that says Module not found: Can't resolve 'next-mdx-import-source-file' and in Next.js 14 you will get a bunch of node_modules/@mdx-js/react/lib related errors in your terminal, so it is mandatory to have that file at the root of the project, even if you don't use it (yet)

Then add the following content:

mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
 
// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including components from
// other libraries.
 
// This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents {
    return {
        // Allows customizing built-in components, e.g. to add styling.
        // h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
        ...components,
    }
}
Note

If you chose to put all your files into an ./src folder, then you need to put the mdx-components.tsx file into the ./src folder and not the root of the project

Now we need to update the content of our next.config.mjs file (in the root of our project) to this:

next.config.mjs
import { withSentryConfig } from '@sentry/nextjs';
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
import createMdx from '@next/mdx'
 
const nextConfig = (phase) => {
 
    const withMDX = createMdx({
        extension: /\.mdx$/,
        options: {
            // optional remark and rehype plugins
            remarkPlugins: [],
            rehypePlugins: [],
        },
    })
 
    /** @type {import('next').NextConfig} */
    const nextConfigOptions = {
        reactStrictMode: true,
        poweredByHeader: false,
        experimental: {
            // experimental typescript "statically typed links"
            // https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes
            // currently false in prod until PR #67824 lands in a stable release
            // https://github.com/vercel/next.js/pull/67824
            typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false,
        },
        headers: async () => {
            return [
                {
                    source: '/(.*)',
                    headers: securityHeadersConfig(phase)
                },
            ];
        },
        // configure `pageExtensions` to include MDX files
        pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
    }
 
    return withMDX(nextConfigOptions)
 
}
Note

This configuration file is probably much shorter than the one you have in your project (if you followed my previous chapters)

I have omitted what we added in the CSP chapter and the Sentry setup so as not to have to display a huge file, but the structure is still the same, so you should be able only to copy the relevant parts (the ones that are highlighted using a different background color)

Line 3: we import the @next/mdx package (that we installed in the previous chapter)

Lines 7 to 13: we configure @next/mdx by passing an object to createMdx; via the options, we add support for remark plugins and rehype plugins (we will add a bunch of those plugins later)

Lines 34 and 35: we tell Next.js what extensions are valid page extensions, the pages we created so far had a .tsx extension, but the new MDX pages we will now add will have the .mdx extension, as you can see we add a bunch more here to cover all use cases. If for example, you are sure you won't use .js as an extension, then you can remove it from the list.

Line 38: we use the withMDX function we just created and pass our nextConfigOptions to it, which will return a configuration with MDX support

Adding markdown support (optional)

The configuration we just did adds support for MDX in pages that have the extension *.mdx, if you want also to add support for markdown pages with the *.md extension, then add the extension in 2 places like so:

next.config.mjs
import { withSentryConfig } from '@sentry/nextjs';
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'
import createMdx from '@next/mdx'
 
const nextConfig = (phase) => {
 
    const withMDX = createMdx({
        extension: /\.mdx?$/,
        options: {
            // optional remark and rehype plugins
            remarkPlugins: [],
            rehypePlugins: [],
        },
    })
 
    /** @type {import('next').NextConfig} */
    const nextConfigOptions = {
        reactStrictMode: true,
        poweredByHeader: false,
        experimental: {
            // experimental typescript "statically typed links"
            // https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes
            // currently false in prod until PR #67824 lands in a stable release
            // https://github.com/vercel/next.js/pull/67824
            typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false,
        },
        headers: async () => {
            return [
                {
                    source: '/(.*)',
                    headers: securityHeadersConfig(phase)
                },
            ];
        },
        // configure `pageExtensions` to include MDX files
        pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx', 'md'],
    }
 
    return withMDX(nextConfigOptions)
 
}

Line 8: we add the extension to the MDX configuration, the regex we use works because we added a question mark behind the x of mdx meaning the x is optional, so md and mdx both are valid extension

Line 35: we add md to the pageExtensions list

Note

It is important to add your new extension to both the MDX configuration and the Next.js pageExtensions

If you don't also add the extension to the createMDX extension option, then @next/mdx won't be able to parse the content of your file

If you don't add your markdown extension to the pageExtensions, then the Next.js app router will not be able to find your file, and it will show a 404

You can use another extension for your markdown pages if you prefer, for example you might prefer using *.markdown, *.mdown, *.mkd, ...

In that case make again sure you make the change in 2 places, let's assume we want to use .markdown instead if .md as extension for our markdown pages, then we would change our code to this for the parser:

next.config.mjs
extension: /\.(md|markdown)$/,

And then the Next.js pageExtensions to this:

next.config.mjs
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx', 'markdown'],

No Rust compiler for MDX (yet)

The Next.js MDX documentation tells us to enable the experimental Rust compiler for MDX:

Next.js supports a new MDX compiler written in Rust. This compiler is still experimental and is not recommended for production use. To use the new compiler, you need to configure next.config.js when you pass it to withMDX

experimental: {
    mdxRs: true,
},

Unfortunately, the documentation did not clearly state that plugins do NOT work if you turn on mdxRs (yet), then there was a ticket and they fixed the docs Docs: Note that needs to turn off mdxRs when using MDX plugins but shortly after they removed the warning again 😭, long story short (no matter if it is in the docs) you can't use plugins when also using mdxRs

When Next.js mentions the Rust compiler then they are talking about SWC, mdxRs the Rust compiler for MDX is the mdxjs-rs package, it uses the markdown-rs package to parse markdown and SWC for everything that is javascript

It is the mdxjs-rs README that has the information that plugins are not supported yet, their readme states that:

This project does not yet support plugins

There is a support ticket you may want to subscribe to if you are interested in getting notifications about the progress for plugin support in markdown-rs: Enable custom plugins #32

Disabling mdxRs and installing @mdx-js/loader instead

Later in this tutorial, we will add several plugins, which is why we are going to turn mdxRs off for now by setting the experimental mdxRs configuration option to false, but also add a comment as to why, so that in the future as soon as support for plugins gets shipped, we just need to turn it on:

next.config.mjs
experimental: {
    // experimental typescript "statically typed links"
    // https://nextjs.org/docs/app/api-reference/next-config-js/typedRoutes
    // currently false in prod until PR #67824 lands in a stable release
    // https://github.com/vercel/next.js/pull/67824
    typedRoutes: phase === PHASE_DEVELOPMENT_SERVER ? true : false,
    // use experimental rust compiler for MDX
    // as of now (07.10.2023) there is no support for rehype & remark plugins
    // this is why it is currently disabled
    mdxRs: false,
},
Note

Of course, if you don't plan on using any plugins like remark-gfm or rehype-pretty-code, then you can turn mdxRs on by setting the option to true

Now that we have disabled mdxRs, we also need to install the MDX webpack plugin manually using the following command:

npm i @mdx-js/loader --save-exact

This webpack plugin will handle parsing MDX and is a replacement for the MDX rust compiler we just disabled

Note

In Next.js 13, the @mdx-js/loader webpack plugin was a dependency of @next/mdx and would get installed when you install @next/mdx

However, since version 14, you need to install the plugin manually as the dependency is now marked as optional

MDX page type (optional)

The following file is not mandatory, but it doesn't hurt to add the right type for MDX pages (I found this in the Next.js "app-dir-mdx" example on GitHub), so (if it doesn't exist already) I recommend creating a types folder in the root of your project and adding a mdx.d.ts file with the following content:

types/mdx.d.ts
declare module '*.mdx' {
    let MDXComponent: (props) => JSX.Element
    export default MDXComponent
}

Because we used create next app in our tsconfig.json, we already have the following include in our tsconfig.json (if it isn't in your tsconfig.json, then you need to add it), which means that *.d.ts files will get included when typescript does the compilation:

tsconfig.json
"include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
],

Tips when using MDX

Now that our MDX setup is done, you might be interested in a few tips that make life easier when writing MDX / markdown in VSCode, if you do check out the "VSCode markdown (and MDX) related tips" chapter in the VSCode post

Congratulations 🎉 you now have MDX support added to your project using next/mdx, in other parts of this tutorial, we will further extend that setup by adding several valuable plugins and learning how to use custom react components to fully benefit from what MDX has to offer

If you liked this post, please consider making a donation ❤️ as it will help me create more content and keep it free for everyone