MDX frontmatter in Gatsby

March 28, 2020

MDX is an incredible toolkit that allows you to write JSX in your Markdown files; creating opportunities for more dynamic and interactive experiences in your content. An example of how MDX could be used:

mdx
1---
2title: My article
3---
4
5import Button from '../components/Button'
6
7Here's some **markdown content**.
8
9<Button>React-powered button</Button>

When I transitioned this site over to MDX, I immediately went to work creating a better Code component, and slew of shortcodes that allowed me to remove a few remark plugins I wanted to bring in-house.

The challenge

Storing additional types of data in Markdown frontmatter is a pretty common-practice; especially with CMS solutions like Netlify CMS. This allows us seperate out chunks you may want to present in different ways - like so:

article.md
mdx
1---
2title: My article
3items:
4 - value: Item 1
5 - value: Item 2
6 - value: Item 3
7---
8
9Article content.
Article.js
js
1const Article = ({ frontmatter, html }) => (
2 <div>
3 <h1>{frontmatter.title}</h1>
4
5 <ul>
6 {frontmatter.items.map((item) => (
7 <li key={item.value}>{item.value}</li>
8 ))}
9 </ul>
10
11 <div dangerouslySetInnerHTML={{ __html: html }} />
12 </div>
13)

But - what if you have Markdown content within your frontmatter - or better yet, MDX?

article.md
mdx
1---
2title: My article
3items:
4 - value: >-
5 Item 1 **value**
6 - value: >-
7 Item 2
8
9 <Button>React-powered button</Button>
10 - value: >-
11 Item 3 __value__
12---
13
14Article content.

Solutions within Gatsby for normal Markdown frontmatter previously led me down the path of creating a runtime component to parse the Markdown strings that would be queried on the frontend:

Data.js
js
1import PropTypes from 'prop-types'
2import remark from 'remark'
3import html from 'remark-html'
4import parser from 'html-react-parser'
5
6// Provides a consistent way for us to render content from Markdown frontmatter that propery encodes entities as well
7const Data = ({ children }) =>
8 parser(remark().use(html).processSync(children).toString())
9
10Data.propTypes = {
11 children: PropTypes.node.isRequired,
12}
13
14export default Data
Article.js
js
1import Data from '../components/Data'
2
3const Article = ({ frontmatter, html }) => (
4 <div>
5 <h1>{frontmatter.title}</h1>
6
7 <ul>
8 {frontmatter.items.map((item) => (
9 <li key={item.value}>
10 <Data>{item.value}</Data>
11 </li>
12 ))}
13 </ul>
14
15 <div dangerouslySetInnerHTML={{ __html: html }} />
16 </div>
17)

The upside to this method was that I could add as many remark plugins as needed - with the obvious downside being the bloat it would add to our bundle. The other downer was gatsby-remark-* plugins that may have additional logic inside of gatsby-browser.js and gatsby-ssr.js wouldn’t be accessible in this component. This implementation was never ideal.

With MDX thrown in the mix, @mdx-js/runtime was closer to a plugin ‘n play solution, but with the downside of being quite large and not recommended for most usecases.

The solution

After struggling with a few different suggestions across the Gatsby community, I came across a method that was straight-forward and easy to extend - enter createSchemaCustomization. This allows us to customize Gatsby’s GraphQL schema by creating type definitions, field extensions or adding third-party schemas.

gatsby-node.js
js
1exports.createSchemaCustomization = ({
2 actions: { createTypes, createFieldExtension },
3 createContentDigest,
4}) => {
5 createFieldExtension({
6 name: 'mdx',
7 extend() {
8 return {
9 type: 'String',
10 resolve(source, args, context, info) {
11 // Grab field
12 const value = source[info.fieldName]
13 // Isolate MDX
14 const mdxType = info.schema.getType('Mdx')
15 // Grab just the body contents of what MDX generates
16 const { resolve } = mdxType.getFields().body
17
18 return resolve({
19 rawBody: value,
20 internal: {
21 contentDigest: createContentDigest(value), // Used for caching
22 },
23 args,
24 context,
25 info,
26 })
27 },
28 }
29 },
30 })
31
32 createTypes(`
33 type Mdx implements Node {
34 frontmatter: MdxFrontmatter
35 }
36
37 type MdxFrontmatter {
38 items: [ItemValues]
39 }
40
41 type ItemValues {
42 value: String @mdx
43 }
44 `)
45}

The above functionality does a few things:

  • Creates a new, mdx field extension that automatically attaches the MDX internals to the fields it is assigned to.
  • Funnels our type definitions down to our specific items frontmatter field.
  • Assigns the newly created mdx field to our value; which is what we want transformed into MDX.

When querying this data with Gatsby’s built-in graphiql tool, what’s returned is the transformed body content, ready to be consumed by <MDXRenderer />.

Article.js
js
1import { MDXProvider } from '@mdx-js/react'
2import { MDXRenderer } from 'gatsby-plugin-mdx'
3
4import Button from '../components/Button'
5
6const Article = ({ frontmatter, body }) => (
7 <MDXProvider components={[Button]}>
8 <div>
9 <h1>{frontmatter.title}</h1>
10
11 <ul>
12 {frontmatter.items.map((item) => (
13 <li key={item.value}>
14 <MDXRenderer>{item.value}</MDXRenderer>
15 </li>
16 ))}
17 </ul>
18
19 <MDXRenderer>{body}</MDXRenderer>
20 </div>
21 </MDXProvider>
22)