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.
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.
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
.
const config = {
plugins: [
{
resolve: `gatsby-plugin-mdx`,
options: {
extensions: [`.mdx`, `.md`],
},
},
],
}
export default config
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.
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
.
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:
- Use the
wrapPageElement
API including its SSR counterpart. - Add an
export default Layout
statement to your MDX file, see MDX documentation on Layout. - 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
-
Install reading-time
into your project:
npm install reading-time
-
In your gatsby-node
add a new field:
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)
})
}
}
-
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:
-
Install @sindresorhus/slugify
into your project:
npm install @sindresorhus/slugify
-
In your gatsby-node
add a new field:
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)}`
})
}
}
-
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
-
Install necessary dependencies into your project:
npm install mdast-util-to-string unist-util-visit
-
Create a new file called remark-headings-plugin.mjs
at the site root:
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
-
Add a new headings
field resolver to your Mdx
nodes through createSchemaCustomization
API:
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,
])
}
-
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
:
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.
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:
- You need to query the absolute path to the MDX file
- 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`)
const { data } = await graphql(`
{
allMdx {
nodes {
id
frontmatter {
slug
}
// highlight-start
internal {
contentFilePath
}
// highlight-end
}
}
}
`)
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`,
})
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
- Instead of querying for the
body
on the MDX node, the page template now receives the transformed MDX as children
property. - 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:
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.
Related