← Back to Notes

Using MDX with NextJS

Hamed Bahram /
12 min read--- views

MDX is a superset of markdown that allows you to use JSX in your markdown files. You can import and embed components in your content to add interactivity to your otherwise static markdown.

Why Markdown?

Markdown often feels more natural to type, its brief syntax can enable you to write content that is both readable and maintainable. Markdown typically looks more like what's intended. So instead of the following HTML:

<blockquote>
  <p>A blockquote with <em>some</em> emphasis.</p>
</blockquote>

you can write:

> A blockquote with _some_ emphasis.

However, markdown is essentially static content, where MDX shines is in its ability to let you use your React components directly in the markup.

Under the Hood

Internally MDX uses remark and rehype. Remark is a markdown processor powered by plugins. Plugins allow you to transform the processed markdown. Rehype is an HTML processor, also powered by plugins. Similar to remark, these plugins let you compile and transform your content.

Using MDX with Next.js

Generally speaking, there are two approaches to integrate MDX in your NextJS project that mainly depends on your data source (where your content is stored) and how you're planning to use components inside your MDX files.

For local files (stored in the local file system), you can use the @next/mdx package from NextJS team. This allows you to create pages directly with the .mdx extension inside your pages/ folder.

For local and/or remote data (stored in a different repo, database or CMS), one option is to use next-mdx-remote package (a third-party package) that allows you to load MDX data inside getStaticProps or getServerSideProps in the same way you would normally load any other data in NextJS.

Let's look at implementing each package in more details.

@next/mdx

As mentioned above, the @next/mdx package sources data from local files, allowing you to create pages with a .mdx extension, directly in your /pages directory.

The following steps are directly from NextJS documentation

  1. Install the required packages:
  npm install @next/mdx @mdx-js/loader
  1. Configure NextJS to support top level .mdx pages:
next.config.js
const withMDX = require('@next/mdx')({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [],
    rehypePlugins: []
  }
})
 
module.exports = withMDX({
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx']
})
  1. Create a new .mdx page in the /pages folder and that's it, you can start writing content and your pages will be served at the corresponding route.

Using Components

Now that your NextJS app is configured to read MDX files you can add React components inside your MDX pages.

import { MyComponent } from '../components/...'
 
# My MDX page
 
This is an unordered list
 
- Item One
- Item Two
- Item Three
 
<section>And here is _markdown_ in **JSX**</section>
 
Checkout my React component
 
<MyComponent />

JS Expressions

MDX supports JavaScript expressions inside curly braces, basically any JS expression that evaluates to something that can be rendered:

Here is a random number: { Math.random() }

Import & Export

MDX also supports ESM import and export statements. Note how we imported an external component while also defining a local one to use in our MDX page.

import { ExternalComponent } from './components/...'
export const LocalComponent = props => (
  <span style={{ color: 'red' }} {...props} />
)
 
<ExternalComponent />
 
# Here is the local component
 
<LocalComponent>Hello...</LocalComponent>.

You can also import data or export variables that can be used inside the MDX file:

import { Chart } from './chart.js'
import sales from './sales.js'
export const year = 2022
 
<Chart data={sales} label={year} />

MDX Content

The main content of an MDX file is exported as a component , this means you can use it inside other components as well as other MDX files. Take example.mdx file:

example.mdx
export const Greet = ({ name }) => <h1>Hello {name}</h1>
 
<Greet name="Alice" />

You can now use the above MDX component in other files:

another.mdx
import Example from './example.mdx'
 
# This is another mdx file
 
<Example />

Instead of importing or defining data within MDX, you can also pass data to your MDX component via props and access them inside your MDX file.

example.mdx
# This is a markdown file expecting props
 
Hello {props.name.toUpperCase()}

It can be imported from another component and passed props like so:

home.jsx
import Link from 'next/link'
import Example from './example.mdx'
 
const Home = () => {
  return <Example name="alice" />
}
 
export default Home

You can do a lot with MDX, as Lee Rob put it in this blog post

You can use the same component-based principles from React and apply them to authoring Markdown documents

For further readings, I recommend checking out the MDX docs.

Layout

To add a layout to your MDX page, you can export a default function from the page and wrap the MDX content with your layout component:

import { Layout } from './components/...'
 
// MDX Content ... //
 
export default ({ children }) => <Layout>{children}</Layout>

Frontmatter

Frontmatter is typically used to store meta data about a page. MDX does not support frontmatter by default, however there are many solutions for adding frontmatter support to MDX, such as gray-matter.

You can also use MDX's ESM import/export feature as an alternative to frontmatter, you can export a meta object from within the .mdx file:

export const meta = {
  title: 'Article Title',
  excerpt: 'An article about such and such...',
  author: 'John Doe'
}
 
# {meta.title}

You might still prefer frontmatter though, as it can be extracted from the file system before compiling your MDX file. For example:

---
title: Hello, there!
---
 
# Some title
 
some other content

Using gray-matter you can access the frontmatter:

import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
 
const root = path.join(process.cwd(), 'pages', 'blog')
 
export const getPostFrontmatter = slug => {
  const realSlug = slug.replace(/\.mdx$/, '')
  const filePath = path.join(root, `${realSlug}.mdx`)
  const fileContent = fs.readFileSync(filePath, 'utf8')
  const { data, content } = matter(fileContent)
  return data // this is the frontmatter
}

As mentioned earlier, MDX doesn't understand frontmatter by default, so we need to tell it to ignore the frontmatter otherwise it'll show up in the final output. We can do so by using a remark plugin, remark-frontmatter. We can add the plugin in our config file.

The only problem is that remark-frontmatter is ESM only package, which means we can't use it in our current commonJS config file. Luckily, from NextJS 12, ES modules are supported in the config file by renaming it to next.config.mjs

next.config.mjs
import nextMDX from '@next/mdx'
import remarkFrontmatter from 'remark-frontmatter'
 
const withMDX = nextMDX({
  extension: /\.mdx?$/,
  options: {
    remarkPlugins: [remarkFrontmatter],
    rehypePlugins: []
  }
})
 
export default withMDX({
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx']
})

Custom Components

So far we've seen how to use components in our MDX files by either importing them or defining them locally. We can also pass components to our MDX files using the components prop. It takes an object mapping component names to the actual components.

example.mdx
# This is a markdown file
 
It is using a custom link component
 
<CustomLink href="/">Home</CustomLink>

The CustomLink component is not imported nor is it defined locally in example.mdx; instead, it's passed to it using the components prop when rendered in the Home page, like so:

home.jsx
import Link from 'next/link'
import Example from './example.mdx'
 
const components = {
  CustomLink: props => <Link {...props} />
}
 
const Home = () => {
  return <Example components={components} />
}
 
export default Home

We can use this technique to add custom styles to any element, since markdown is ultimately compiled to native HTML elements, we can create our own custom components that map to regular HTML tags.

import Link from 'next/link'
import Image from 'next/image'
import Heading from './components/...'
 
const components = {
  h1: Heading.H1,
  h2: Heading.H2,
  a: props => <Link {...props} />,
  img: props => <Image alt={props.alt} layout="fill" {...props} />
  // ...
}

MDX Provider

Passing components through props is fine but it can become cumbersome. To solve this, we can use @mdx-js/react which is a context provider that makes our custom components available to all of our MDX files.

To set this up:

  1. Install the @mdx-js/react package
  2. Specify the providerImportSource in your config file:
next.config.mjs
const withMDX = nextMDX({
  // ...
  options: {
    providerImportSource: '@mdx-js/react'
  }
})
  1. Use MDXProvider to wrap your highest level MDX component or the _app.js and pass a components object as a prop so all MDX pages can access your custom components.
import Link from 'next/link'
import Image from 'next/image'
import Heading from './components/...'
import { MDXProvider } from '@mdx-js/react'
 
const components = {
  h1: Heading.H1,
  h2: Heading.H2,
  a: props => <Link {...props} />,
  img: props => <Image alt={props.alt} layout="fill" {...props} />
  // ...
}
 
function MyApp({ Component, pageProps }) {
  return (
    <MDXProvider components={components}>
      <Component {...pageProps} />
    </MDXProvider>
  )
}
 
export default MyApp

That's it for @next/mdx, you can get started with the with-mdx example template which uses this package to try out writing some MDX content.

Now let's look at the next-mdx-remote package, how it's implemented and how it differs from the first solution.

next-mdx-remote

You can use next-mdx-remote to load MDX content through getStaticProps or getServerSideProps, the same way you would load any other data.

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 } }
}

This package exposes a function serialize which is intended to run server side and a component, <MDXRemote/> for the client side.

serialize consumes a string of MDX and returns an object that should be passed to <MDXRemote/> directly.

You can also optionally pass options:

serialize(source, {
  scope: {},
  mdxOptions: {
    remarkPlugins: [],
    rehypePlugins: [],
    hastPlugins: [],
    compilers: [],
    filepath: '/some/file/path'
  },
  parseFrontmatter: false
})
  • The scope object is used to pass props to the components used inside the MDX file. scope can also be passed to <MDXRemote/> component as a prop.
  • mdxOptions are passed directly to the MDX compiler.
  • parseFrontmatter indicates whether or not to parse the frontmatter from the mdx source.

<MDXRemote/> consumes the output of the serialize function as well as optional components prop. Its result can be rendered directly in your components.

<MDXRemote
  compiledSource={string}
  components={object}
  scope={object}
  lazy={boolean}
/>
  • compiledSource is exposed via the object returned from serialize.
  • components takes a components object to be used inside your MDX file.
  • scope exposed either through serialize function or directly passed to the <MDXRemote/> as a prop, is used to provide props to the components used inside the MDX file.
  • lazy takes a boolean and will defer hydration of the content if set to true to improve initial load time by immediately serving static markup.

Caveats

When using next-mdx-remote, ESM import and export statements cannot be used inside your MDX files. If you need to use components in your MDX files, they should be provided as a prop to <MDXRemote/> or via context.

MDX Provider

If you want to make components available to any <MDXRemote/> being rendered in your application, you can use <MDXProvider/> from @mdx-js/react. You can also replace HTML tags by custom components in the same way.

Summary

When using next-mdx-remote you are not limited to storing your MDX files locally. Also you are not bound to the filesystem based routing where moving MDX pages around will also change the page's route (URL).

While this gives you flexibility on where you store your MDX files, it is not as powerful as next/mdx when it comes to MDX features such as using ESM import/export, JS expressions, exporting and importing MDX content as components etc.

If you have a lot of files that all import, define and use different components, or if you want to use the same component-based principles from React and apply them to your markdown, you may benefit from using next/mdx as next-mdx-remote is limited in that area. On the other hand, if you want the freedom to load your data from within getStaticProps or getServerSideProps, regardless of the source, in the same way you would load any other data in NextJS, next-mdx-remote would be a better fit.

Resources

Here are some of the resources that inspired this note:

Articles

Tutorials