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:

yarn add gatsby-plugin-mdx

Add the plugin to the config file:

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

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:

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:

src
└── content
│ └── blog
│ └── page1
│ └── index.mdx
└── pages
│ └── blog
│ └── index.mdx
└── templates
└── 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
plugins: [
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/pages`,
name: `pages`
}
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/src/content`,
name: `content`
}
}
}
]

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
const path = require(`path`);
const { createFilePath } = require(`gatsby-source-filesystem`);

exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
// reference to our template file
const blogPost = path.resolve(`./src/templates/blog-post.tsx`);
// updated graphql query to look for mdx output
const result = await graphql(`
{
allMdx(
sort: { fields: [frontmatter___date], order: DESC }
limit: 1000
) {
edges {
node {
fields {
slug
}
frontmatter {
title
date
type
url
}
}
}
}
}
`);

if (result.errors) {
throw result.errors
}

// retrieve all posts from result
const posts = result.data.allMdx.edges;

// construct each blog post page
posts.forEach((page, index) => {
const previous = index === posts.length - 1 ? null : posts[index + 1].node;
const next = index === 0 ? null : posts[index - 1].node;
createPage({
path: page.node.fields.slug,
// template component reference
component: blogPost,
context: {
slug: page.node.fields.slug,
previous,
next
}
});
});
};

// insert nodes at build time for all mdx files (slug in this case)
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
if (node.internal.type === `Mdx`) {
const value = createFilePath({ node, getNode });
createNodeField({
name: `slug`,
node,
value
});
}
}

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
const defaultPage = path.resolve(`./src/templates/default-template.tsx`);
const pages = result.data.allMdx.edges;
const posts = [];
const defaults = [];
pages.forEach((item) => {
switch (item.node.frontmatter.type) {
case "blog":
posts.push(item);
break;

default:
defaults.push(item);
break;
}
});

// iterate over the other types and use the correct template
defaults.forEach((page) => {
createPage({
path: page.node.fields.slug,
// added default template here
component: defaultPage,
context: {
slug: page.node.fields.slug
}
})
});

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
const BlogIndex: React.FC<IBlogIndexProps> = ({ data }) => {
const siteTitle = data.site.siteMetadata.title;
const posts = data.allMdx.edges;

return (
<Layout title={siteTitle}>
{
posts.map(({ node }: any) => {
return (
<article key={node.fields.slug}>
<header>
<h3>
<Link to={`${node.fields.slug}`}>
{node.frontmatter.title}
</Link>
</h3>
<small>{node.frontmatter.date}</small>
</header>
<section>
<p
dangerouslySetInnerHTML={{
__html: node.frontmatter.description || node.excerpt,
}}
/>
</section>
</article>
);
})
}
</Layout>
);
};

export default BlogIndex;

// updated query to reference allMdx property
export const pageQuery = graphql`
query {
site {
siteMetadata {
title
}
}
allMdx(
sort: { fields: [frontmatter___date], order: DESC }
filter: { frontmatter: { type: {eq: "blog"}}}
) {
edges {
node {
excerpt
fields {
slug
}
frontmatter {
date(formatString: "MMMM DD, YYYY")
title
description
}
}
}
}
}
`;

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

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

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

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

{...body}

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

templates/blog-post.tsx
const BlogPostTemplate: React.FC<IBlogPostProps> = ({ data, pageContext }) => {
const post = data.mdx
const siteTitle = data.site.siteMetadata.title

return (
<Layout title={siteTitle}>
<article>
<header>
<h1>{post.frontmatter.title}</h1>
<p>{post.frontmatter.date}</p>
</header>
{/* This is the renderer for our MDX body */}
<MDXRenderer>{post.body}</MDXRenderer>
</article>
</Layout>
);
}

export default BlogPostTemplate;

// update query to reference the mdx property
export const pageQuery = graphql`
query BlogPostBySlug($slug: String!) {
site {
siteMetadata {
title
}
}
mdx(fields: { slug: { eq: $slug } }) {
id
excerpt(pruneLength: 160)
body
frontmatter {
title
date(formatString: "MMMM DD, YYYY")
description
}
}
}
`;

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.