In Prod

← Back to Posts

Adding MDX to Gatsby

February 09, 2020

Gatsby supports markdown for page content out of the box and the framework is also build on top of react. The MDX format is an extension of markdown which allows jsx/tsx components to be added to the content.


Install MDX

Install the MDX plugin:


_1
yarn add gatsby-plugin-mdx

Add the plugin to the config file:

gatsby-config.js

_21
module.exports = {
_21
// ...metadata
_21
plugins: [
_21
// ...rest
_21
{
_21
resolve: `gatsby-plugin-mdx`,
_21
options: {
_21
extensions: [`.mdx`, `.md`],
_21
gatsbyRemarkPlugins: [
_21
{
_21
resolve: `gatsby-remark-images`,
_21
options: {
_21
maxWidth: 590
_21
}
_21
},
_21
`gatsby-remark-prismjs`
_21
]
_21
}
_21
}
_21
]
_21
}

The options provided above tell the plugin to:

  1. Look for both md and mdx files
  2. Setup some Remark processing which should:
  • Set maxWidth restrictions on images
  • Format code sections

Prism comes with some themes out of the box. I updated mine to a dark theme by changing the default import in gatsby-browser.js to import the tomorrow theme:


_1
import "prismjs/themes/prism-tomorrow.css";


Configure MDX

To support automatic sub-routes for blog posts or other lists (e.g. mysite.com/blog/page1), I found that the easiest structure was to have my tsx in one folder and the mdx in another:


_10
src
_10
└── content
_10
│ └── blog
_10
│ └── page1
_10
│ └── index.mdx
_10
└── pages
_10
│ └── blog
_10
│ └── index.mdx
_10
└── templates
_10
└── template1.tsx

This may be unnecessary, but other ways seemed to need a lot more configuration.

Update gatsby-config.js to point the gatsby-source-filesystem plugin to where your mdx and page files are:

gatsby-config.js

_17
plugins: [
_17
{
_17
resolve: `gatsby-source-filesystem`,
_17
options: {
_17
path: `${__dirname}/src/pages`,
_17
name: `pages`
_17
}
_17
},
_17
{
_17
resolve: `gatsby-source-filesystem`,
_17
options: {
_17
path: `${__dirname}/src/content`,
_17
name: `content`
_17
}
_17
}
_17
}
_17
]

At this point our project will support mdx, but it still needs to be added to templating.


Templating

For all post files to be passed through a template at build time, we need to configure gatsby-node.js to point all relevant files to our template file:

gatsby-node.js

_67
const path = require(`path`);
_67
const { createFilePath } = require(`gatsby-source-filesystem`);
_67
_67
exports.createPages = async ({ graphql, actions }) => {
_67
const { createPage } = actions;
_67
// reference to our template file
_67
const blogPost = path.resolve(`./src/templates/blog-post.tsx`);
_67
// updated graphql query to look for mdx output
_67
const result = await graphql(`
_67
{
_67
allMdx(
_67
sort: { fields: [frontmatter___date], order: DESC }
_67
limit: 1000
_67
) {
_67
edges {
_67
node {
_67
fields {
_67
slug
_67
}
_67
frontmatter {
_67
title
_67
date
_67
type
_67
url
_67
}
_67
}
_67
}
_67
}
_67
}
_67
`);
_67
_67
if (result.errors) {
_67
throw result.errors
_67
}
_67
_67
// retrieve all posts from result
_67
const posts = result.data.allMdx.edges;
_67
_67
// construct each blog post page
_67
posts.forEach((page, index) => {
_67
const previous = index === posts.length - 1 ? null : posts[index + 1].node;
_67
const next = index === 0 ? null : posts[index - 1].node;
_67
createPage({
_67
path: page.node.fields.slug,
_67
// template component reference
_67
component: blogPost,
_67
context: {
_67
slug: page.node.fields.slug,
_67
previous,
_67
next
_67
}
_67
});
_67
});
_67
};
_67
_67
// insert nodes at build time for all mdx files (slug in this case)
_67
exports.onCreateNode = ({ node, actions, getNode }) => {
_67
const { createNodeField } = actions;
_67
if (node.internal.type === `Mdx`) {
_67
const value = createFilePath({ node, getNode });
_67
createNodeField({
_67
name: `slug`,
_67
node,
_67
value
_67
});
_67
}
_67
}

This will make sure that we are using the correct template for our mdx files.

I have multiple template types and mdx file containers, so I am just switching over a type property within the mdx files frontmatter and pushing to a default template if not present:

gatsby-node.js

_27
const defaultPage = path.resolve(`./src/templates/default-template.tsx`);
_27
const pages = result.data.allMdx.edges;
_27
const posts = [];
_27
const defaults = [];
_27
pages.forEach((item) => {
_27
switch (item.node.frontmatter.type) {
_27
case "blog":
_27
posts.push(item);
_27
break;
_27
_27
default:
_27
defaults.push(item);
_27
break;
_27
}
_27
});
_27
_27
// iterate over the other types and use the correct template
_27
defaults.forEach((page) => {
_27
createPage({
_27
path: page.node.fields.slug,
_27
// added default template here
_27
component: defaultPage,
_27
context: {
_27
slug: page.node.fields.slug
_27
}
_27
})
_27
});

We can now create an pages/blog/index.tsx file that will render out a list of blog posts using frontmatter:

pages/blog/index.tsx

_63
const BlogIndex: React.FC<IBlogIndexProps> = ({ data }) => {
_63
const siteTitle = data.site.siteMetadata.title;
_63
const posts = data.allMdx.edges;
_63
_63
return (
_63
<Layout title={siteTitle}>
_63
{
_63
posts.map(({ node }: any) => {
_63
return (
_63
<article key={node.fields.slug}>
_63
<header>
_63
<h3>
_63
<Link to={`${node.fields.slug}`}>
_63
{node.frontmatter.title}
_63
</Link>
_63
</h3>
_63
<small>{node.frontmatter.date}</small>
_63
</header>
_63
<section>
_63
<p
_63
dangerouslySetInnerHTML={{
_63
__html: node.frontmatter.description || node.excerpt,
_63
}}
_63
/>
_63
</section>
_63
</article>
_63
);
_63
})
_63
}
_63
</Layout>
_63
);
_63
};
_63
_63
export default BlogIndex;
_63
_63
// updated query to reference allMdx property
_63
export const pageQuery = graphql`
_63
query {
_63
site {
_63
siteMetadata {
_63
title
_63
}
_63
}
_63
allMdx(
_63
sort: { fields: [frontmatter___date], order: DESC }
_63
filter: { frontmatter: { type: {eq: "blog"}}}
_63
) {
_63
edges {
_63
node {
_63
excerpt
_63
fields {
_63
slug
_63
}
_63
frontmatter {
_63
date(formatString: "MMMM DD, YYYY")
_63
title
_63
description
_63
}
_63
}
_63
}
_63
}
_63
}
_63
`;

Each post can now live as a folder/index.mdx pair in the content directory.


_7
src
_7
└── content
_7
└── blog
_7
└── page1
_7
│ └── index.mdx
_7
└── page2
_7
└── index.mdx

The frontmatter needs to be the first thing in each mdx file:

index.mdx

_9
// every index.mdx file
_9
---
_9
title: A title
_9
date: 2020-02-09
_9
description: A description
_9
type: blog
_9
---
_9
_9
{...body}

Our blog-post.tsx template file can now consume this data and display the body as required:

templates/blog-post.tsx

_40
const BlogPostTemplate: React.FC<IBlogPostProps> = ({ data, pageContext }) => {
_40
const post = data.mdx
_40
const siteTitle = data.site.siteMetadata.title
_40
_40
return (
_40
<Layout title={siteTitle}>
_40
<article>
_40
<header>
_40
<h1>{post.frontmatter.title}</h1>
_40
<p>{post.frontmatter.date}</p>
_40
</header>
_40
{/* This is the renderer for our MDX body */}
_40
<MDXRenderer>{post.body}</MDXRenderer>
_40
</article>
_40
</Layout>
_40
);
_40
}
_40
_40
export default BlogPostTemplate;
_40
_40
// update query to reference the mdx property
_40
export const pageQuery = graphql`
_40
query BlogPostBySlug($slug: String!) {
_40
site {
_40
siteMetadata {
_40
title
_40
}
_40
}
_40
mdx(fields: { slug: { eq: $slug } }) {
_40
id
_40
excerpt(pruneLength: 160)
_40
body
_40
frontmatter {
_40
title
_40
date(formatString: "MMMM DD, YYYY")
_40
description
_40
}
_40
}
_40
}
_40
`;

We should now have our mdx posts being rendered correctly within the correct sub-route.

Below is a demo using a simple button component that uses react hooks to increment a counter. It has been imported directly into the mdx file for this post and works as expected:

Click the button below to increment
Count: 0

Andrew McMahon
These are a few of my insignificant productions
by Andrew McMahon.