clubmate.fi

A good[ish] website

Web development blog, loads of UI and JavaScript topics

How to MDX with Gatsby

Filed under: JavaScript— Tagged with: gatsby

An introduction to MDX, and how to configure and set it up on an existing Gatsby site.

MDX explained briefly

MDX is like Markdown, but it also renders React components: if you type in <div>foo<div>, MDX will render a component. This means that you need to close all your elements, or MDX will crash, it works just like React.

In MDX there are two keywords that have meaning: import and export. Other than that (and components), anything you write in an MDX document will be printed as text. For example const foo = ['baz', 'quix'] has no meaning in MDX and will just printed out as a string.

Installing MDX to Gatsby project

Install the needed packages:

$ npm install --save gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react

Configuring MDX

gatsby-plugin-mdx will be replacing gatsby-transformer-remark entirely. In my case it included the following changes to the gatsby-config.js file:

{
- resolve: 'gatsby-transformer-remark',
+ resolve: 'gatsby-plugin-mdx',
  options: {
+   extensions: ['.mdx', '.md'],
-   excerpt_separator: '<!-- more -->',
-   plugins: [
+   gatsbyRemarkPlugins: [
      'gatsby-remark-responsive-iframe',
      {
        resolve: 'gatsby-remark-autolink-headers',
        options: {
          offsetY: 0
        }
      },
      {
        resolve: 'gatsby-remark-prismjs',
        options: {
          classPrefix: 'language-',
          inlineCodeMarker: null
        }
      }
    ]
  }
}

I’m using gatsby-remark-autolink-headers and gatsby-remark-prismjs plugins, for you that section might look different.

Unfortunately, at the time of writing this, gatsby-plugin-mdx has no way of handling excerpt separator: excerpt_separator: '<!-- more -->'. I found an discussion in the Gatsby GitHub, but not much else.

gatsby-transformer-remark can be now safely uninstalled:

$ npm uninstall gatsby-transformer-remark gatsby-plugin-feed

Update templates and queries

Since allMarkdownRemark is now gone, all references to it has to be updated, because gatsby-plugin-mdx uses different naming internally. Your code editor’s search and replace abilities come handy here, I simply searched and replaced these strings using VSCode:

  • Replace allMarkdownRemark with allMdx
  • Replace 'MarkdownRemark' with 'Mdx'
  • Replace markdownRemark with mdx
  • Replace rawMarkdownBody with rawBody (if you’re using this)

Remember to do case sensitive matching. I just blasted those through pretty carelessly and I didn’t break anything, but check that your replacement isn’t too greedy etc.

Setup MDX rendering

Import the MDXRenderer in the file where you’re rendering the blog posts:

import { MDXRenderer } from 'gatsby-plugin-mdx'

Then, somewhere in your GraphQL query you’ve got the post.html that holds the contents for a post, replace that with post.body, and pass it into the MDXRenderer React component:

- <div dangerouslySetInnerHTML={{ __html: post.html }} />
+ <MDXRenderer>{post.body}</MDXRenderer>

That’s pretty much it, restart the dev server and see if it works. As a bonus, getting rid of dangerouslySetInnerHTML feels right.

For me that was just the beginning, I needed to sort out a ton of old posts that didn’t work properly with MDX.

Fixing rendering problems with legacy posts

I had few hundred aging entries to deal with, originally imported from WordPress, and some of them had HTML elements that weren’t closed properly, or CSS written inside <style> tags that made the build process crash, giving me an absolute deluge or errors which was hard to make sene of.

So I removed the line extensions: ['.mdx', '.md] from the gatsby-config (gatsby-plugin-mdx defaults to .mdx file extension), then changed the extensions one-by-one to .mdx on all of the posts, this way I could see exactly which post had an error.

Importing components

The importing works just as you’d expect, here’s an example .mdx file:

---
title: My cool post
date: 2020-11-24 23:00
---

import { PullQuote } from '../src/components/PullQuote'

## Interesting title

Lorem ipsum dolor...

<PullQuote source="Something interesting" />

If the PullQuote component is used a lot, it can be included as a kind of a global, also referred to as shortcodes. This can be done with the MDXProvider.

Make the sure the MDXProvider wraps the component that renders the blog post, in my case it’s a component called Post inside my BlogPost template:

import React from 'react'
import { Link } from 'gatsby'
import { YouTube } from '../../components/Embeds'
import PullQuote from '../../components/PullQuote'

const shortCodes = { Link, YouTube, PullQuote }

const BlogPost = props => (
  <Base>
    <PostWrapper>
      <MDXProvider components={shortCodes}>
        <Post post={props.data.mdx} />
      </MDXProvider>
      <Sidebar />
    </PostWrapper>
  </Base>
)

Defining variables

You can’t do const foo = 'bar' that would be printed as is. Only two native JavaScript keywords can be used: import and export. So you can do:

export const listOfThings = ['chair', 'plank', 'dog', 'pea']

Then use your variable anywhere in the post.

How to run Markdown inside HTML components

Maybe you’ve got something like this:

<Note type="info">
  Lorem ipsum dolor. The `flexbox` is cool. [Read about it](/box-flex).
</Note>

The Markdown inside that component is not executed, it doesn’t matter if it’s a custom React component or a normal HTML element.

To make the Markdown parsing work inside an MDX component, simply add line breaks inside the element:

<Note type="info">

Lorem ipsum dolor ym sit. The `flexbox` is cool. [Read about it](/box-flex).

</Note>

How to write CSS into a style tag

This is not very React-like, but maybe you’ve got some legacy posts that have style tags in them etc. (I had a bunch of those).

If you write CSS into a style tag normally, it crashes the parser:

<style>
  .some-class {
    color: red;
  }
</style>

Here’s how you can fix that, wrap your CSS to a template string {...}:

<style>{`
  .some-class {
    color: red;
  }
`}</style>

You can also put the CSS to another file and import it:

import './my-post-styles.css'

Speaking of importing post specific assets.

Keep post specific assets in the post’s directory

Not specific to MDX but somehow I’ve been using more files with MDX than before. In Gatsby, posts are files, but a post can also be a directory, the directory’s name is picked up as the slug. This way the post dir can hold all the assets related to it. index.mdx being the entry point:

posts/
├── how-to-mdx-in-gatsby/
│   ├── data.json
│   ├── Graph.js
│   ├── index.mdx
│   └── styles.js
├── foo-bar.mdx
├── bar-foo.mdx
.
.

Or if you name your file foo-bar.mdx instead of index.mdx, you can access it from how-to-mdx-in-gatsby/foo-bar, neat.

Returning functions from MDX components

You can’t really return functions from MDX components, but you can run an IIFE (immediately invoked function expression) inside a component which is part of an MDX file, this way you can define variables and have state and what not.

Here’s a random example that sets a div’s content:

---
title: Some post
---

Lorem ipsum dolor:

<div>
  {(() => {
    const [divText, setDivText] = React.useState('Hello!')
    return (
      <>
        <div
          contenteditable
          onInput={event => setDivText(event.target.innerText)}
          className="module"
        >
          Hello!
        </div>
        <div className="module__output">
          <strong>Value:</strong> {divText}
        </div>
      </>
    )
  })()}
</div>

If you find yourself doing this a lot, it might better to have it as an external component.

Pass in data as props

You can pass any props you want into the MDXRenderer component:

<MDXRenderer foo="FOO BAR">{body}</MDXRenderer>

Then access the props in the MDX files like it would be a React component:

---
title: Foo
---

Lorem ipsum <span>{props.foo}</span> dolor.

If you don’t want to use an HTML element, React fragment should also work <>{props.foo}</>.

Layouts

You can have different layout per post, if you so desire. This is done by exporting component from your post, the post is then wrapped by the template code.

export default ({ children }) => (
  <div>
    <h1>Custom layout</h1>
    <div>{children}</div>
  </div>
)

## An MDX doc with a custom layout

Content here...

Or likewise it can be imported if that’s more convenient.

import CustomLayout from './src/components/custom-layout'

export default CustomLayout

## An MDX doc with a custom layout

Content here...

MDX v2

The v2 will be released shortly, at the time of writing this 2.0.0-rc.2 is out. gatsby-plugin-mdx has not been updated yet to v2. The v2 is apparently completely rewritten, it’s much faster, smaller, and lighter.

Improved syntax in v2

The up-and-coming v2 has some useful additions, most exciting probably being that you can now write MarkDown inside HTML components, the following will parse how you would expect:

<div>*hi*?</div>

<div># hi?</div>

<main>
  <div>

    # hi?

  </div>
</main>

JavaScript expressions in v2

The v2 lets you run JavaScript inside curly braces {}:

PI times two is {2 \* Math.PI}

This means, if you want to type out the left curly brace, you need to escape it \{ or use the expression {'{'}. Same goes for the less than symbol <, btw.

See a more thorough roundup on the NDX site.

Some MDX gotchas to keep in mind

Here’s some things that you might have issues with.

Imports are cached

If you import a module from mdx file: import Foo from './Foo', then rename it: import Bar from './Bar', Gatsby will throw an error similar to this:

Module build failed (from ./node_modules/gatsby/dist/utils/babel-loader.js):
Error: ENOENT: no such file or directory, open '/Users/bob/gatsby-blog/contents/posts/my-post/Foo.js'

The error persist if you restart the dev server. This is because the Gatsby’s caching system can’t properly handle imports to MDX files. To fix it, run gatsby clean and boot up the dev server. I have the clean command as a script in my package.json file: npm run clean.

Starting lines with html elements

Not really a gotcha, but something to keep in mind: if you start a line with an HTML element, it won’t be wrapped in <p>. Not sure if this is a general Markdown things or just MDX.

Normal sentence lorem ipsum.

<span>This line</span> won’t be wrapped in a paragraph.

There are probably many other edge cases, here’s just few to mention.

Conclusions

MDX has changed how blog posts are written, I quite like it. And the v2 is going to be pretty amazing.

Comments would go here, but the commenting system isn’t ready yet, sorry.

  • © 2022 Antti Hiljá
  • About
  • All rights reserved yadda yadda.
  • I can put just about anything here, no one reads the footer anyways.
  • I love u!