Blog A.Wolf

How to create a Gatsby Theme with MDX support?

How to create a Gatsby Theme with MDX support?

July 14th, 2019 - 12 min read

This post is based on the live stream from Jason Lengstorf. If you like you can watch the video here but I'm covering everything in this post.

I have changed the structure a bit so it's better to read. So it's not strictly following the video step by step but you're getting the same result in the end.

So what are we creating here? We will create a Gatsby Theme with yarn workspaces and we're using MDX files as the data source. You can use this setup for creating a style-guide or a blog etc.

Why Yarn workspaces?

If you have used something like lerna.js you probably know the benefits of a Mono-repo and how to manage packages in a single repository. Yarn workspaces are like Lerna but it is already included in Yarn - so no need to install anything if you're already using Yarn.

It's easier to develop the theme if it's located close to a usage example.

You could also create a Gatsby theme with-out a mono repo but the development experience is better with the workspaces.

Later if you publish your theme as an NPM package you can add your theme to your next project by just running yarn add @your-name/gatsby-theme-mdx or you could add it as a dependency from Github with yarn add https://github.com/user-name/gatsby-theme-mdx/tree/master/theme. Then you don't need to publish the theme to NPM.

Setup

Folders

  1. Create a project directory e.g. gatsby-theme-mdx-demo
  2. Create a package.jon with yarn init -y in the root folder of gatsby-theme-mdx-demo and add the following content:

    {
    ...,
    "private": true,
    "workspaces": [
    "demo",
    "theme"
    ]
    }

    This will tell Yarn that there are two workspace packages to use demo and theme.

  3. Create the workspace folders demo and theme with mkdir or in VS code
  4. Go into theme folder and run yarn init -y Modify the package.json data and update name, author & keywords. For name choose @your-name/gatsby-theme-mdx and for the keywords create an array with the following items ["gatsby", "gatsby-plugins", "gatsby-theme"] The keywords and the name are required if you'd like to publish your theme so others can use it. A good guide on how to publish a theme can be found at egghead. Note: If you're publishing a scoped package you must pass access parameter to make it public - it defaults to private e.g. npm publish --access public
  5. Create an empty index.js file into theme directory - just to have it.

Install dependencies

Our project structure is created and we need to install the required dependencies.

  1. Add the theme peer dependencies with yarn workspace @your-name/gatsby-theme-mdx add -P gatsby react react-dom. This will add the Gatsby related dependencies to the Theme - use the name you have chosen in package.json.

    Important use -P to add them as peerDependency and use the right workspace (here our theme workspace).

  2. Create a file gatsby-node.js in theme folder just to check that it's working:

    exports.createPages = (_, options) => {
    console.log("IT WORKS");
    }
  3. Goto demo directory and call yarn init -y

  4. Install regular dependencies with yarn workspace demo add gatsby react react-dom in our demo.

  5. Now add the theme to the demo by calling yarn workspace demo add @your-name/gatsby-theme-mdx@*

    Note: The add * is needed because our theme is not published yet and we'd like to take any available version. No need to quote the * on Windows.

  6. With yarn workspaces info we can check that it's using just our local theme which is what we want here.

  7. We'd like to run our demo but we first need to add a script that we can execute. Add the following to package.json in demo workspace:

    {
    ...
    "scripts": {
    "develop": "gatsby develop",
    "clean": "gatsby clean"
    }
    }

    We're also adding gatsby clean so we can easily clear the cache if it is required or we could even add it to develop script like gatsby clean && gatsby develop. But I think it's also OK to keep it separate.

  8. Add a gatsby-config.js file to the root of the demo so we can use our theme with the following code:

    module.exports = {
    plugins: [{
      resolve: '@your-name/gatsby-theme-mdx',
      options: { /* basePath: '/myCoolStuff' */ }
    }]
    }

    Note: basePath is optional it is just an example - so it's OK to remove it here. It can be used so that you can access your pages with /myCoolStuff/pageName prefixed.

  9. Now start the dev server by calling yarn workspace demo develop. So in the console, there should be a log IT WORKS between the standard Gatsby output. If it does not check your gatsby-config that you're correctly resolving your theme and that there is no typo in the name.

Configure MDX plugin

You can find every detail in the docs of Gatsby-plugin-mdx.

If you don't know MDX you can read about it here - in short, it is an extended Markdown version that can render React components.

The following steps are required to set up the MDX plugin:

  1. Add the MDX dependencies to our theme yarn workspace @your-name/gatsby-theme-mdx add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react
  2. We also need gatsby-source-filesystem plugin as a dependency and we add it with yarn workspace @your-name/gatsby-theme-mdx add gatsby-source-filesystem. So we can load the MDX files later with a GraphQL query.
  3. Create a gatsby-config.js file in theme to use the plugin with the following code:
module.exports = options => ({
  plugins: [
    {
      resolve: 'gatsby-plugin-mdx',
      options: {}
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: options.contentPath || 'docs'
      }
    }
  ]
})

This will configure the filesystem plugin to load the MDX files from the contentPath folder or a default folder docs. We don't have that folder yet so we're having two options:

  • Create the docs folder manually in the demo or
  • Let the theme create the folder with Node.js if it doesn't exist

Bootstrap our demo app

After adding the theme we need to prepare the app so the theme will find the MDX files. Therefore we're creating the folder if it doesn't exists and for it will use the node package mkdirp.

  1. So add it with yarn workspace @your-name/gatsby-theme-mdx add mkdirp.
  2. Open gatsby-node.js of theme in your editor and add the API hook onPreBootstrap:

    const fs = require('fs')
    const path = require('path')
    const mkdirp = require('mkdirp')
    // ...
    /*
    Runs before Gatsby does things. 
    It's similar to a preInstall hook in npm scripts.
    */
    exports.onPreBootstrap = ({ store }, options) => {
    const { program } = store.getState() // gatsby internal state to get the current execution folder
    const contentPath = options.contentPath || 'docs'
    const dir = path.join(program.directory, contentPath)
    if (!fs.existsSync(dir)) {
    mkdirp.sync(dir)
    }
    }

We use the onPreBootstrap hook to check if the content directory docs exists and if not we'll create it. So we can avoid the filesystem plugin to fail because of a missing directory.

  1. With yarn workspace demo develop we can test if the docs folder will be correctly created for us.
  2. Let's create a first MDX file in the docs folder by adding docs/accessibility.mdx:

    \# Accessibility
  3. We check if we can access the MDX file in the GraphQL explorer by going to http://localhost:8000/___graphql and check the following query:

    {
    allMdx {
    nodes {
      rawBody
    }
    }
    }

After clicking on the play button we should get the content of the previously create MDX file. If not check that filesystem plugin is in the config and if there are any errors in the console.

Use MDX data in the template

So to use the MDX data in the page we have to create our first template in theme/src/templates/default.js and add:

import React from 'react'

const Default = props => <pre>{JSON.stringify(props, null, 2)}</pre>

export default Default

We'd like to create a slug for each MDX file and that can be added by using onCreateNode API hook. That hook will be called by Gatsby for every MDX node Gatbsy will create and we can create a slug for each one based on its location on disk. The code in gatsby-node.js for it is:

// ...

exports.onCreateNode = ({ node, actions }, options) => {
  // We could also use 'Mdx' here but there is no relativePath in the data, that's why we're looking for a File here
  if (node.internal.type !== 'File') {
    return
  }

  // Get post path
  const toPostPath = fileNode => {
    // Using parse from Node path to get the directory of the file
    const { dir } = path.parse(fileNode.relativePath)
    const basePath = options.basePath || ''
    const url = [basePath, dir, fileNode.name].filter(
      name => name && name !== ''
    )
    return url.join('/')
  }

  const slug = toPostPath(node)
  actions.createNodeField({
    node,
    name: 'slug',
    value: slug
  })
}

Note: I've changed the path joining code a bit because on Windows path.join will use backslashes and we don't want that here - so code from the video will work on Mac & Linux. I replaced the logic to join an array with filtering out empty strings to avoid additional slashes. That should also work on Mac/Linux - not tested.

Now restart yarn workspace demo develop and check the GraphQl browser if you can see the slug. If not try to run yarn workspace demo clean and restart the dev server.

Modify the createPages function inside theme/gatsby-node.js created in step 2 and use the following code:

// ...
exports.createPages = async ({ actions, graphql, reporter }, options) => {
  const result = await graphql(`
    query {
      allFile {
        nodes {
          fields {
            slug
          }
        }
      }
    }
  `)

  if (result.errors) {
    // Oh no
    reporter.panic('Error loading mdx files', result.errors)
  }

  result.data.allFile.nodes.forEach(node => {
    actions.createPage({
      path: node.fields.slug,
      component: require.resolve('./src/templates/default.js'),
      context: {
        slug: node.fields.slug
      }
    })
  })
}

// ...

We're using a Gatbsy built-in reporter for error handling here. It will stop the build and report the error to the console.

If everything is working properly you should now have the MDX file data rendered with the default template if you're going to localhost:8000/accessibility with your browser. The page will just display the pre-tag with the passed props.

We'd like to query the childMdx in our default template with the following code:

import { graphql } from "gatsby"
export const query = graphql`
  {
    query($slug: String!) {
      file(fields: { slug: { eq: $slug } }) {
        childMdx {
          body
        }
      }
    }
  }
`

Slug is a Graphql variable of type string which is mandatory. Gatsby will fail if it's not available. Slug is available because we have created the field before.

With this query, we can shadow components. You can read about component shadowing in the Gatsby post What is Component Shadowing?.

Instead of context, we're getting the queried data in our template. You can now go to the /accessibility page from before and you should see a data key with file/childMdx/body keys.

Next, create a component in theme/src/components called Page.js with:

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

const Page = ({ body }) => <MDXRenderer>{body}</MDXRenderer>

export default Page

This will render the MDX into the page and execute the React code of the MDX file.

Go back to default template and add the Page component:

// ... other imports
import Page from "../components/Page";

// ... query ...

const Default = ({ data }) => {
  // The page object is an abstraction to have a flat object that we're using in our component. That makes later changes easier.
  const page = {
    body: data.file.childMdx.body
  }

  return <Page {...page} />
}

Rendered MDX file Rendered MDX

Setup ready

Setup is ready now and you can use your components by importing them in your MDX files like:

  • import Button from "@your-name/gatsby-theme-mdx/src/components/Button" for components that are living in your theme.
  • import Wave from "../src/components/Wave" for components that are added to your current project.

You can give it a try now by adding a component at both locations and use them in your docs.

If you're getting stuck at some point with the imports you can have a look at the linked Github repo below.

Code & Sandbox

You can find the source code on Github https://github.com/AWolf81/gatsby-theme-mdx-demo and a codesandbox with the Theme here.

To create the Codesandbox just open the url https://codesandbox.io/s/github/AWolf81/gatsby-theme-mdx-demo/tree/master/demo. This will create a sandbox based on the demo of the theme project. I'm not sure if the Theme must be published to NPM as I had the error 502: Bad Gateway. That's why I've published it to NPM.

This error could be only fixed by forking the sandbox.

What's next

From here you can add theming with Theme-UI (see resources below) or add frontmatter for a title or tags.

Or you can start a blog or style-guide with the setup. For a blog here are some ideas that could be interesting to add:

  • Add reading time plugin
  • Use Prismjs plugin to highlight code blocks
  • Set up Twitter cards
  • Add prev. or next post navigation buttons
  • Add some static content like an Impressum, privacy note, etc.
  • Add share buttons so your user can easily share your content on Twitter/Facebook...
  • And probably more...

I've created a post about my Gatsby blog setup where I'm explaining how I've added some of the points mentioned above. For the share button, I've created a separate post here. You can also find a link to the source code of my blog in the setup post so you can check everything on Github.

You could also improve the setup by adding prettier, eslint & husky as mentioned in the following post. This will help to keep your code consistent. You can add these as devDependencies to the root of your project.

Another improvement could be to add index.mdx handling e.g. example/index is probably not what you want. So you can try to add this to the slug creation.

Theme UI Resources

©2019 Alexander Wolf