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.
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.
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.
Our project structure is created and we need to install the required dependencies.
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).
gatsby-node.js
in theme
folder just to check that it's working:exports.createPages = (_, options) => {
console.log("IT WORKS");
}
demo
directory and call yarn init -y
yarn workspace demo add gatsby react react-dom
in our demo.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.
yarn workspaces info
we can check that it's using just our local theme which is what we want here.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.
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.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:
yarn workspace @your-name/gatsby-theme-mdx add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react
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.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:
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.
yarn workspace @your-name/gatsby-theme-mdx add mkdirp
.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.
3. With yarn workspace demo develop
we can test if the docs
folder will be correctly created for us.
4. Let's create a first MDX file in the docs folder by adding docs/accessibility.mdx
:
\# Accessibility
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.
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
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.
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.
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:
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.