Migrate to Netlify Today

Netlify announces the next evolution of Gatsby Cloud. Learn more

ContactSign Up
Official Plugin
View plugin on GitHub

gatsby-plugin-mdx

gatsby-plugin-mdx is the official integration for using MDX with Gatsby.

MDX is markdown for the component era. It lets you write JSX embedded inside markdown. It’s a great combination because it allows you to use markdown’s often terse syntax (such as # heading) for the little things and JSX for more advanced components.

Table of contents

Installation

npm install gatsby-plugin-mdx gatsby-source-filesystem @mdx-js/react

Usage

This README assumes you’re using gatsby@5.3.0 or later. If you’re using an older Gatsby version or don’t want to use ESM for your Gatsby files, refer to this older version of the README.

After installing gatsby-plugin-mdx you can add it to your plugins list in your gatsby-config.

We highly recommend using ES Modules (ESM) syntax for your gatsby-config file as it’ll enable you to use the latest packages from the unified ecosystem.

You’ll also want to configure gatsby-source-filesystem to point at your src/pages directory.

gatsby-config.mjs
import { dirname } from "path"
import { fileURLToPath } from "url"

const __dirname = dirname(fileURLToPath(import.meta.url))

const config = {
  plugins: [
    `gatsby-plugin-mdx`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `pages`,
        path: `${__dirname}/src/pages`,
      },
    },
  ],
}

export default config

By default, this configuration will allow you to automatically create pages with .mdx files in src/pages.

If you have MDX files in another location than src/pages you’ll need to add another instance of gatsby-source-filesystem and configure the path to point at this folder. This is necessary for MDX files that you want to import into React components or for files you want to query via GraphQL.

Please Note:

  • gatsby-plugin-mdx requires gatsby-source-filesystem to be present and configured to process local MDX files in order to generate the resulting Gatsby nodes (gatsby-source-filesystem needs to discover all MDX files in order to create MDX nodes and allow the processing for each of them).
  • MDX syntax differs from Markdown as it only supports CommonMark by default. Nonstandard markdown features like GitHub flavored markdown (GFM) can be enabled with plugins (see mdxOptions instructions). GFM includes features like tables or footnotes.
  • Certain features like HTML syntax doesn’t work in MDX. Read the “What is MDX?” guide to learn more.

To automatically create pages from MDX files outside of src/pages you’ll need to configure gatsby-plugin-page-creator and gatsby-source-filesystem to point to this folder of files.

gatsby-config.mjs
import { dirname } from "path"
import { fileURLToPath } from "url"

const __dirname = dirname(fileURLToPath(import.meta.url))

const config = {
  plugins: [
    `gatsby-plugin-mdx`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `posts`,
        path: `${__dirname}/src/posts`,
      },
    },
    {
      resolve: `gatsby-plugin-page-creator`,
      options: {
        path: `${__dirname}/src/posts`,
      },
    },
  ],
}

export default config

Also check out the guide Adding MDX Pages for more details.

Configuration

gatsby-plugin-mdx exposes a configuration API that can be used similarly to any other Gatsby plugin. You can define MDX extensions, layouts, global scope, and more.

Key Default Description
extensions [".mdx"] Configure the file extensions that gatsby-plugin-mdx will process
gatsbyRemarkPlugins [] Use Gatsby-specific remark plugins
mdxOptions {} Options directly passed to compile() of @mdx-js/mdx

Extensions

By default, only files with the .mdx file extension are treated as MDX when using gatsby-source-filesystem. To use .md or other file extensions, you can define an array of file extensions in the gatsby-plugin-mdx section of your gatsby-config.js.

gatsby-config.mjs
const config = {
  plugins: [
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        extensions: [`.mdx`, `.md`],
      },
    },
  ],
}

export default config

gatsby-remark-* plugins

This config option is used for compatibility with a set of plugins many people use with remark that require the Gatsby environment to function properly. In some cases, like gatsby-remark-prismjs, it makes more sense to use a library like prism-react-renderer to render codeblocks using a React component. In other cases, like gatsby-remark-images, the interaction with the Gatsby APIs is well deserved because the images can be optimized by Gatsby and you should continue using it.

When using these gatsby-remark-* plugins, be sure to also install their required peer dependencies. You can find that information in their respective README.

gatsby-config.mjs
const config = {
  plugins: [
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        gatsbyRemarkPlugins: [
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 590,
            },
          },
        ],
      },
    },
  ],
}

export default config

Using a string reference is also supported for gatsbyRemarkPlugins.

gatsbyRemarkPlugins: [`gatsby-remark-images`]

mdxOptions

These configuration options are directly passed into the MDX compiler.

See all available options in the official documentation of @mdx-js/mdx.

gatsby-config.mjs
import remarkGfm from "remark-gfm"
import remarkExternalLinks from "remark-external-links"
import rehypeSlug from "rehype-slug"
import rehypeAutolinkHeadings from "rehype-autolink-headings"

const config = {
  plugins: [
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        mdxOptions: {
          remarkPlugins: [
            // Add GitHub Flavored Markdown (GFM) support
            remarkGfm,
            // To pass options, use a 2-element array with the
            // configuration in an object in the second element
            [remarkExternalLinks, { target: false }],
          ],
          rehypePlugins: [
            // Generate heading ids for rehype-autolink-headings
            rehypeSlug,
            // To pass options, use a 2-element array with the
            // configuration in an object in the second element
            [rehypeAutolinkHeadings, { behavior: `wrap` }],
          ],
        },
      },
    },
  ],
}

export default config

Please Note: Most of the remark/rehype/unified ecosystem is published as ESM which means that you have to use ES Modules (ESM) and Gatsby.

Imports

When importing a React component into your MDX, you can import it using the import statement like in JavaScript.

import { SketchPicker } from "react-color"

# Hello, world!

Here's a color picker!

<SketchPicker />

Note: You should restart gatsby develop to update imports in MDX files. Otherwise, you’ll get a ReferenceError for new imports. You can use the shortcodes approach if that is an issue for you.

Layouts

You can use regular layout components to apply layout to your sub pages.

To inject them, you have several options:

  1. Use the wrapPageElement API including its SSR counterpart.
  2. Add an export default Layout statement to your MDX file, see MDX documentation on Layout.
  3. When using the createPage action to programatically create pages, you should use the following URI pattern for your page component: your-layout-component.js?__contentFilePath=absolute-path-to-your-mdx-file.mdx. To learn more about this, head to the programmatically creating pages guide.

Programmatically create MDX pages

Read the MDX documentation on programmatically creating pages to learn more.

GraphQL MDX Node structure

In your GraphQL schema, you will discover several additional data related to your MDX content. While your local GraphiQL will give you the most recent data, here are the most relevant properties of the Mdx entities:

Property Description
frontmatter Sub-entity with all frontmatter data. Regular Gatsby transformations apply, like you can format dates directly within the query.
excerpt A pruned variant of your content. By default trimmed to 140 characters. Based on rehype-infer-description-meta.
tableOfContents Generates a recursive object structure to reflect a table of contents. Based on mdast-util-toc.
body The raw MDX body (so the MDX file without frontmatter)
internal.contentFilePath The absolute path to the MDX file (useful for passing it to ?__contentFilePath query param for layouts). Equivalent to the absolutePath on File nodes.

Extending the GraphQL MDX nodes

Use the createNodeField action to extend MDX nodes. All new items will be placed under the fields key. You can alias your fields to have them at the root of the GraphQL node.

timeToRead

  1. Install reading-time into your project:

    npm install reading-time
  2. In your gatsby-node add a new field:

    gatsby-node.mjs
    import readingTime from "reading-time"
    
    export const onCreateNode = ({ node, actions }) => {
      const { createNodeField } = actions
      if (node.internal.type === `Mdx`) {
        createNodeField({
          node,
          name: `timeToRead`,
          value: readingTime(node.body)
        })
      }
    }
  3. You’re now able to query the information on the MDX node:

    query {
      mdx {
        fields {
          timeToRead {
            minutes
            text
            time
            words
          }
        }
      }
    }

wordCount

See timeToRead. It returns timeToRead.words.

slug

This largely comes down to your own preference and how you want to wire things up. This here is one of many possible solutions to this:

  1. Install @sindresorhus/slugify into your project:

    npm install @sindresorhus/slugify
  2. In your gatsby-node add a new field:

    gatsby-node.mjs
    import slugify from "@sindresorhus/slugify"
    
    export const onCreateNode = ({ node, actions }) => {
      const { createNodeField } = actions
      if (node.internal.type === `Mdx`) {
        createNodeField({
          node,
          name: `slug`,
          value: `/${slugify(node.frontmatter.title)}`
        })
      }
    }
  3. You’re now able to query the information on the MDX node:

    query {
      mdx {
        fields {
          slug
        }
      }
    }

If you don’t want to use the frontmatter.title, adjust what you input to slugify(). For example, if you want information from the File node, you could use getNode(node.parent).

headings

  1. Install necessary dependencies into your project:

    npm install mdast-util-to-string unist-util-visit
  2. Create a new file called remark-headings-plugin.mjs at the site root:

    remark-headings-plugin.mjs
    import { visit } from "unist-util-visit"
    import { toString } from "mdast-util-to-string"
    
    const transformer = (tree, file) => {
      let headings = []
    
      visit(tree, `heading`, heading => {
        headings.push({
          value: toString(heading),
          depth: heading.depth,
        })
      })
    
      const mdxFile = file
      if (!mdxFile.data.meta) {
        mdxFile.data.meta = {}
      }
    
      mdxFile.data.meta.headings = headings
    }
    
    const remarkHeadingsPlugin = () => transformer
    
    export default remarkHeadingsPlugin
  3. Add a new headings field resolver to your Mdx nodes through createSchemaCustomization API:

    gatsby-node.mjs
    import { compileMDXWithCustomOptions } from "gatsby-plugin-mdx"
    import remarkHeadingsPlugin from "./remark-headings-plugin.mjs"
    
    export const createSchemaCustomization = async ({ getNode, getNodesByType, pathPrefix, reporter, cache, actions, schema, store }) => {
      const { createTypes } = actions
    
      const headingsResolver = schema.buildObjectType({
        name: `Mdx`,
        fields: {
          headings: {
            type: `[MdxHeading]`,
            async resolve(mdxNode) {
              const fileNode = getNode(mdxNode.parent)
    
              if (!fileNode) {
                return null
              }
    
              const result = await compileMDXWithCustomOptions(
                {
                  source: mdxNode.body,
                  absolutePath: fileNode.absolutePath,
                },
                {
                  pluginOptions: {},
                  customOptions: {
                    mdxOptions: {
                      remarkPlugins: [remarkHeadingsPlugin],
                    },
                  },
                  getNode,
                  getNodesByType,
                  pathPrefix,
                  reporter,
                  cache,
                  store,
                }
              )
    
              if (!result) {
                return null
              }
    
              return result.metadata.headings
            }
          }
        }
      })
    
      createTypes([
        `
          type MdxHeading {
            value: String
            depth: Int
          }
        `,
        headingsResolver,
      ])
    }
  4. You’re now able to query the information on the MDX node:

    query {
      mdx {
        headings {
          value
          depth
        }
      }
    }

Components

MDX and gatsby-plugin-mdx use components for different things like rendering and component mappings.

MDXProvider

MDXProvider is a React component that allows you to replace the rendering of tags in MDX content. It does this by providing a list of components via context to the internal MDXTag component that handles rendering of base tags like p and h1.

import { MDXProvider } from "@mdx-js/react"

const MyH1 = props => <h1 style={{ color: `tomato` }} {...props} />
const MyParagraph = props => (
  <p style={{ fontSize: "18px", lineHeight: 1.6 }} {...props} />
)

const components = {
  h1: MyH1,
  p: MyParagraph,
}

export const ComponentsWrapper = ({ children }) => (
  <MDXProvider components={components}>{children}</MDXProvider>
)

The following components can be customized with the MDXProvider:

Tag Name Syntax
p Paragraph
h1 Heading 1 #
h2 Heading 2 ##
h3 Heading 3 ###
h4 Heading 4 ####
h5 Heading 5 #####
h6 Heading 6 ######
thematicBreak Thematic break ***
blockquote Blockquote >
ul List -
ol Ordered list 1.
li List item
table Table `---
tr Table row `This
td/th Table cell
pre Pre
code Code
em Emphasis _emphasis_
strong Strong **strong**
delete Delete ~~strikethrough~~
hr Break ---
a Link <https://mdxjs.com> or [MDX](https://mdxjs.com)
img Image ![alt](https://mdx-logo.now.sh)

It’s important to define the components you pass in a stable way so that the references don’t change if you want to be able to navigate to a hash. That’s why we defined components outside of any render functions in these examples.

Shortcodes

If you want to allow usage of a component from anywhere (often referred to as a shortcode), you can pass it to the MDXProvider.

src/components/layout.jsx
import React from "react"
import { MDXProvider } from "@mdx-js/react"
import { Link } from "gatsby"
import { YouTube, Twitter, TomatoBox } from "./ui"

const shortcodes = { Link, YouTube, Twitter, TomatoBox }

export const Layout = ({ children }) => (
  <MDXProvider components={shortcodes}>{children}</MDXProvider>
)

Then, in any MDX file, you can navigate using Link and render YouTube, Twitter, and TomatoBox components without an import.

# Hello, world!

Here's a YouTube embed

<TomatoBox>
  <YouTube id="123abc" />
</TomatoBox>

Read more about injecting your own components in the official MDX provider guide.

Migrating from v3 to v4

gatsby-plugin-mdx@^4.0.0 is a complete rewrite of the original plugin with the goal of making the plugin faster, compatible with MDX v2, leaner, and more maintainable. While doing this rewrite we took the opportunity to fix long-standing issues and remove some functionalities that we now think should be handled by the user, not the plugin. In doing so there will be of course breaking changes you’ll have to handle – but with the help of this migration guide and the codemods you’ll be on the new version in no time!

Updating dependencies

npm remove @mdx-js/mdx
npm install gatsby-plugin-mdx@latest @mdx-js/react@latest

If you used any related plugins like gatsby-remark-images, also update them to their @latest version.

New options in gatsby-config

  • Move your remarkPlugins and rehypePlugins keys into the new mdxOptions config option:

    const config = {
      plugins: [
        {
          resolve: `gatsby-plugin-mdx`,
          options: {
    -       remarkPlugins: [],
    -       rehypePlugins: [],
    +       mdxOptions: {
    +         remarkPlugins: [],
    +         rehypePlugins: [],
    +       },
          },
        },
      ],
    }
    
    export default config
  • There’s a new option called mdxOptions which is passed directly to the MDX compiler. See all available options in the official documentation of @mdx-js/mdx.

  • Only extensions, gatsbyRemarkPlugins, and mdxOptions exist as options now. Every other option got removed, including defaultLayouts. See the layouts guide to learn how to use layouts with gatsby-plugin-mdx@^4.0.0.

  • Make sure that any gatsby-remark-* plugins are only listed inside the gatsbyRemarkPlugins array of gatsby-plugin-mdx, not inside the plugins array of gatsby-config or in any other place.

GFM & ESM-only packages

  • GitHub flavored markdown (GFM) support was removed from MDX v2. You can re-enable it with mdxOptions (you have to install remark-gfm)
  • Most of the remark ecosystem is ESM so just using the latest package version of remark-*/rehype-* most probably won’t work. Check out the workarounds mentioned in mdxOptions

Updating createPage action in gatsby-node

In most cases the changes necessary here are related to the removal of defaultLayouts option and the new way how layouts are done. See the layouts guide to learn how to use layouts with gatsby-plugin-mdx@^4.0.0.

You’ll need to do two things to continue using your old layout file:

  1. You need to query the absolute path to the MDX file
  2. You have to attach this MDX file via the __contentFilePath query param to your layout file
const postTemplate = path.resolve(`./src/templates/post.jsx`)

actions.createPage({
-  component: postTemplate,
+  component: `${postTemplate}?__contentFilePath=/path/to/content.mdx`,
})

Or a more complete example:

const postTemplate = path.resolve(`./src/templates/post.jsx`)

// Rest of createPages API...

const { data } = await graphql(`
  {
    allMdx {
      nodes {
        id
        frontmatter {
          slug
        }
        internal {          contentFilePath        }      }
    }
  }
`)

data.allMdx.nodes.forEach(node => {
  actions.createPage({
    path: node.frontmatter.slug,
    component: `${postTemplate}?__contentFilePath=${node.internal.contentFilePath}`,    context: {
      id: node.id,
    },
  })
})

You’ll also need to update your template file itself, see the next section Updating page templates.

Note: You could also directly pass the MDX file to the component like this:

actions.createPage({
  component: `/path/to/content.mdx`,
  // If you don't want to construct it yourself, use internal.contentFilePath
  // component: node.internal.contentFilePath
})

However, we’d recommend placing such files into the src/pages directory and gatsby-plugin-mdx will handle the page creation itself.

Updating page templates

  1. Instead of querying for the body on the MDX node, the page template now receives the transformed MDX as children property.
  2. You no longer need to use <MDXRenderer> as you can use {children} directly.
import React from "react"
import { graphql } from "gatsby"
- import { MDXRenderer } from "gatsby-plugin-mdx"

- function PostTemplate({ data: { mdx } }) {
+ function PostTemplate({ data: { mdx }, children }) {

  return (
    <main>
      <h1>{mdx.frontmatter.title}</h1>
-     <MDXRenderer>
-       {mdx.body}
-     </MDXRenderer>
+     {children}
    </main>
  )
}

 export const pageQuery = graphql`
  query PostTemplate($id: String!) {
    mdx(id: { eq: $id }) {
      frontmatter {
        title
      }
-     body
    }
  }
`

export default PostTemplate

Updating MDX content

As MDX v2 changed the way it handles content you might need to update your MDX files to be valid MDX now. See their Update MDX content guide for all details. In What is MDX? it is also laid out which features don’t work. Most importantly for this migration:

  • HTML syntax doesn’t work in MDX as it’s replaced by JSX (<img> to <img />). Instead of HTML comments, you can use JavaScript comments in braces: {/* comment! */}
  • Unescaped left angle bracket / less than (<) and left curly brace ({) have to be escaped: \< or \{ (or use expressions: {'<'}, {'{'})
    • If you’re using the enableCustomId option from gatsby-remark-autolink-headers you’ll run into problems due to the above. You need to disable this option and use rehype-slug-custom-id instead.

In our testing, most of the time the issue were curly brackets that needed to be escaped with backticks:

- You can upload this to Git{Hub,Lab}
+ You can upload this to `Git{Hub,Lab}`

You can also use eslint-mdx to find all culprits and in some cases automatically fix them through ESLint.

Updating MDX nodes

Since most MDX nodes are moved to userland you’ll have to extend the GraphQL MDX nodes and update your queries accordingly. However, you can alias your fields to have them at the root of the GraphQL node.

Here’s an example of an updated query (if you re-implemented most features):

 {
-  timeToRead
-  rawBody
-  slug
   headings
-  html
-  mdxAST
-  wordCount
-  fileAbsolutePath
+  body
+  fields {
+    timeToRead
+    slug
+  }
+  internal {
+    contentFilePath
+  }
 }

Here’s an example on how you’d alias your fields to keep the shape of the MDX node the same:

gatsby-node.mjs
import readingTime from "reading-time"

export const onCreateNode = ({ node, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `Mdx`) {
    createNodeField({
      node,
      name: `timeToRead`,
      value: readingTime(node.body)
    })
  }
}

export const createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  createTypes(`#graphql
    type Mdx implements Node {
      # You can also use other keys from fields.timeToRead if you don't want "minutes"
      timeToRead: Float @proxy(from: "fields.timeToRead.minutes")
      wordCount: Int @proxy(from: "fields.timeToRead.words")
    }
  `)
}

v3 to v4: Breaking Changes

  • Removed plugin options: defaultLayouts, mediaTypes, lessBabel, shouldBlockNodeFromTransformation, commonmark
  • Moved plugin options remarkPlugins and rehypePlugins into mdxOptions
  • Removed timeToRead, rawBody, slug, headings, html, mdxAST, wordCount, fileAbsolutePath from the query result. You can check Extending the GraphQL MDX nodes to learn how to re-implement some of them on your own. Also check Updating MDX nodes for guidance on changing your queries
  • gatsby-plugin-mdx only applies to local files (that are sourced with gatsby-source-filesystem)
  • Removed the ability to use js and json in frontmatter
  • Loading MDX from other sources as the filesystem is not supported. If you have a need for that, please comment in the GitHub Discussion
  • All MDX v2 migration notes apply

As mentioned above the html field was removed from the GraphQL node. We know that some of you used this for e.g. gatsby-plugin-feed. Unfortunately, for compatibility and performance reasons we had to remove it. We recommend using the excerpt field in the meantime until we find a feasible solution to provide MDX rendered as HTML. If you have any suggestions, please comment on the GitHub Discussion.

Why MDX?

Before MDX, some of the benefits of writing Markdown were lost when integrating with JSX. Implementations were often template string-based which required lots of escaping and cumbersome syntax.

MDX seeks to make writing with Markdown and JSX simpler while being more expressive. Writing is fun again when you combine components, that can even be dynamic or load data, with the simplicity of Markdown for long-form content.

© 2023 Gatsby, Inc.