A good[ish] website
Web development blog, loads of UI and JavaScript topics
An introduction to MDX, and how to configure and set it up on an existing Gatsby site.
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.
Install the needed packages:
$ npm install --save gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react
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
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:
allMarkdownRemark
with allMdx
'MarkdownRemark'
with 'Mdx'
markdownRemark
with mdx
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.
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.
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.
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>
)
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.
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>
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.
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.
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.
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}</>
.
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...
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.
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>
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.
Here’s some things that you might have issues with.
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
.
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.
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.