Markdown/MDX with Next.js

Lee Robinson

If you're creating an information-dense website like documentation or a blog, you're probably considering using Markdown. Most developers are familiar with Markdown from GitHub and other online communities.

Markdown allows you to transform plaintext into formatted elements. For example–you want to write a 2000 character blog post, including rich formatting options like bold text, italicized text, and links. You'd like to optimize for writing and spend less time coding. And let's be honest – writing clearly is more difficult. Rather than writing HTML like this:

<p>
  I <strong>love</strong> using <a href="https://nextjs.org/">Next.js</a>
</p>

Instead, you can use Markdown to express your styling:

I **love** using [Next.js](https://nextjs.org/)

This guide will explain different ways to use Markdown and MDX with Next.js in five sections:

Transformation

To use Markdown with Next.js, you must first transform your Markdown content into something React can understand. Given some Markdown input, we want to output JSX inside a component. The most popular solution is using remark.

Remark parses and compiles Markdown using an AST. It's powered by plugins that allow you to add syntax highlighting, generate a table of contents, and more. For example, you can use remark-html to transform Markdown to HTML.

There's also a superset of Markdown called MDX, which allows you to write JSX inside of your Markdown. You can use the same component-based principles from React and apply them to authoring Markdown documents. MDX shines when you need dynamic content for each Markdown file (e.g. dynamic global state like a language dropdown) or rich, interactive experiences with interactive components throughout. It's also convenient to be able to use JavaScript functions inside your files.

Data Source

Your Markdown files are either local (in the file system) or remote (stored in a CMS). Local content is a great choice when you want to allow collaboration (e.g. edit this page on GitHub link) with a large number of people. They also make more sense for small, personal projects. Remote content might also be a better choice if non-developers need to edit content.

To use local Markdown content with Next.js, you can transform your source files (/posts/my-post.md) to HTML using remark and remark-html. These Node libraries can be used inside getStaticProps / getStaticPaths / getServerSideProps. For example, the blog-starter example reads all the Markdown files in the _posts directory and generates a unique page for each slug. This solution works for both local and remote data. If your data is remote, you'd simply read from a CMS instead of your file system.

To use MDX with Next.js, your approach will differ based on your data source location. For local content, you can use the @next/mdx package. This allows you to create pages directly with the .mdx extension inside your pages/ folder. For remote data, one option is to use next-mdx-remote (a community project) to fetch your Markdown content inside getStaticProps / getStaticPaths.

Styling

If you're using CSS or Sass, you can target your HTML elements inside of the component containing the Markdown. For example, Tailwind Typography will automatically style the HTML elements generated from your Markdown inside an element with the prose class.

<article class="prose lg:prose-xl">
  <h1>Garlic bread with cheese: What the science tells us</h1>
  <p>
    For years parents have espoused the health benefits of eating garlic bread with cheese to their
    children, with the food earning such an iconic status in our culture that kids will often dress
    up as warm, cheesy loaf for Halloween.
  </p>
  <p>
    But a recent study shows that the celebrated appetizer may be linked to a series of rabies cases
    springing up around the country.
  </p>
  <!-- ... -->
</article>

If you're using CSS-in-JS or want more granular control at the component level, another option is next-mdx-remote. This also allows you to use custom components inside your Markdown (e.g. Heading) by providing an object with components for the Markdown to transform to.

// pages/index.js

import renderToString from 'next-mdx-remote/render-to-string'
import hydrate from 'next-mdx-remote/hydrate'

import Heading from '../components/heading'

const components = { Heading }

export default function Post({ source }) {
  const content = hydrate(source, { components })
  return <div className="wrapper">{content}</div>
}

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

Or @next/mdx using an MDXProvider.

// pages/index.js

import { MDXProvider } from '@mdx-js/react'
import Image from 'next/image'
import { Heading, Text, Pre, Code, Table } from '../components'

const components = {
  img: Image,
  h1: Heading.H1,
  h2: Heading.H2,
  p: Text,
  code: Pre,
  inlineCode: Code
}

export default function Post(props) {
  return (
    <MDXProvider components={components}>
      <main {...props} />
    </MDXProvider>
  )
}

Sharing Layout

When using an approach that has dynamic routing (e.g. /pages/doc/[slug].js), you can wrap the returned React component with a shared <Layout />. Since it's just a React component, you could have separate layouts like <BlogLayout /> or <DocsLayout />.

// pages/docs/[slug].js

import Layout from '../components/Layout'
import { getAllDocs, getDocBySlug } from '../lib/docs'
import markdownToHtml from '../lib/markdown'

export default function Doc({ meta, content }) {
  return <Layout meta={meta}>{content}</Layout>
}

export async function getStaticProps({ params }) {
  const doc = getDocBySlug(params.slug)
  const content = await markdownToHtml(doc.content || '')

  return {
    props: {
      ...doc,
      content
    }
  }
}

export async function getStaticPaths() {
  const docs = getAllDocs()

  return {
    paths: docs.map(doc => {
      return {
        params: {
          slug: doc.slug
        }
      }
    }),
    fallback: false
  }
}

When using @next/mdx and file-system based routing, you can make your Layout the default export of the file.

// pages/posts/markdown-with-next.mdx

import Layout from '../components/Layout'

export const meta = {
  title: 'Markdown/MDX with Next.js',
  author: 'Lee Robinson'
}

I **love** using [Next.js](https://nextjs.org/)

export default ({ children }) => <Layout meta={meta}>{children}</Layout>

Metadata

Metadata allows you to describe the contents of your Markdown file. For example, adding a title and an author of a blog post. If you're using Markdown, gray-matter allows you to write a YAML front-matter like so:

---
title: Markdown/MDX with Next.js
author: Lee Robinson
---

I **love** using [Next.js](https://nextjs.org/)

Which can then be transformed into a JS object when reading your files.

import matter from 'gray-matter'

const docsDirectory = join(process.cwd(), 'docs')

export function getDocBySlug(slug) {
  const realSlug = slug.replace(/\.md$/, '')
  const fullPath = join(docsDirectory, `${realSlug}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')
  const { data, content } = matter(fileContents)

  return { slug: realSlug, meta: data, content }
}

If you're using MDX, you can use a JS object directly.

// pages/posts/markdown-with-next.mdx

export const meta = {
  title: 'Markdown/MDX with Next.js',
  author: 'Lee Robinson'
}

Conclusion

There are many different ways to use Markdown or MDX with Next.js. For more complete examples, see: