Building a blog with Gatsby and MDX

I've long considered trying out blogging. Now, I'm finally giving it a go.

This first post is all about how I built this blog feature into my site using Gatsby and MDX. For background context, I built this site about two years ago using Gatsby. Given that it is essentially a single landing page with various social links, using Gatsby wasn't entirely necessary, but I had been looking for an excuse to try it out. Fast forward two years, I've decided to make further use of Gatsby and leverage it to add a blog to the site. Here's how I went about it.


The basic idea behind this project is to use Gatsby plugins and APIs to dynamically generate blog pages from MDX files within our project. After our project is configured, adding a new blog post will be as easy as creating a new MDX file. The plugins we'll use include the gatsby-source-filesystem plugin and gatsby-plugin-mdx along with the createPages Gatsby Node API. After setting those up, we'll then take a brief look at how to template our posts and style our MDX elements. So how does all this work? Let's jump in.

Note: This blog post assumes basic Gatsby and React knowledge. If you are unfamiliar with Gatsby, there is a great tutorial series available on Gatsby's site to get you up to speed. Additionally, I will not be setting the project up step by step, but will provide enough information that you could easily integrate a similar blog feature into any existing Gatsby project.

Setting up

Our first requirement is the gatsby-source-filesystem plugin. What this plugin allows us to do is to use our project's filesystem as a source for data in our application. We will create a posts directory that will store our MDX files. We will then use gatsby-source-filesystem to query for those files and convert the content into blog posts.

Our second requirement is going to be the gatsby-plugin-mdx plugin. This is the official integration for using MDX within Gatsby. If you are unfamiliar with MDX, it is essentially a file format that allows you to combine standard Markdown with JSX. I won't go into much detail but it is pretty neat, especially if you are a React developer and are used to writing JSX. You can read more about MDX here. This is the file format we'll use to write our posts.

After installing both plugins, we can configure them in our gatsby-config.js file as follows.

1module.exports = {
2 ...,
3 plugins: [
4 {
5 resolve: `gatsby-source-filesystem`,
6 options: {
7 name: `posts`,
8 path: `${__dirname}/src/posts/`,
9 },
10 },
11 `gatsby-plugin-mdx`,
12 ],

The path value here is src/posts/ which is where I will store my MDX files. This configuration tells Gatsby to query this particular directory for data.

Now that we have our plugins configured, our project is ready to query for MDX files and turn them into blog pages. But just how do we tell it to do that?

The answer is Gatsby's createPages API. If you are familiar with Gatsby, you know that by default, Gatsby core creates pages for any React files that it finds in the src/pages/ directory. Similarly, this API allows us to instruct Gatsby to create additional pages based on the criteria we specify. When all is said and done, our gatsby-node.js file (located in the project's root directory) will look as follows:

1const path = require("path")
3exports.createPages = async ({ graphql, actions, reporter }) => {
4 // destructure the createPage function from the actions object
5 const { createPage } = actions
7 const result = await graphql(`
8 query {
9 allMdx {
10 edges {
11 node {
12 id
13 frontmatter {
14 slug
15 }
16 }
17 }
18 }
19 }
20 `)
22 if (result.errors) {
23 reporter.panicOnBuild('🚨 ERROR: Loading "createPages" query')
24 }
26 // create blog post pages
27 const posts = result.data.allMdx.edges
29 // call `createPage` for each result
30 posts.forEach(({ node }, index) => {
31 createPage({
32 path: node.frontmatter.slug,
33 component: path.resolve(`./src/components/posts-page-layout.js`),
34 // you can use the values in this context in
35 // our page layout component
36 context: { id: node.id },
37 })
38 })

In the code above, we first query our filesystem for MDX files starting on line 7. These will be located in the directory we specified in our earlier configuration. Each node in our query results is an MDX file that was found.

Then starting on line 30, for each MDX file (or "post"), we call createPage, passing it a path value which will serve as our post's URL, a component value which will be used as our page template, and a context object which can hold additional data to be made available to us within our template component.

You may have noticed that on line 13, within each file result, we expect something called frontmatter. Frontmatter is a set of key-value pairs that can be used to provide additional data about the file. In our case, we're going to use frontmatter for storing information about the blog, including details such as our posts title, slug, date, author, and more. Frontmatter is denoted in a Markdown file by three dashes at the start and end of a block.

Creating posts from MDX files

Now that the project is configured to dynamically create pages from MDX files, we need to create our MDX file within the directory we specified our gatsby-source-filesystem configuration (src/posts/). If you are using a slug frontmatter key-value pair for your path value as we are, the name of the file is not so important, as long as it is an MDX file. For consistency, I will give it the same name as the slug (src/posts/building-a-blog-with-gatsby-and-mdx.mdx). Make special note of the frontmatter at the top of the file, located between the --- lines. The slug value is what will make your post available at http://<yourdomain>/<slug>. We will make use of the rest of the frontmatter data in our page template.

2title: Building a blog with Gatsby and MDX
3slug: building-a-blog-with-gatsby-and-mdx
4author: Adam Goth
7Blog content here...

The last missing piece that our createPages function expects is the template component that we specified as the component value in our createPage call. The value we passed is ./src/components/posts-page-layout.js, so let's go ahead and create that file.

1import React from "react"
2import { graphql } from "gatsby"
3import { MDXProvider } from "@mdx-js/react"
4import { MDXRenderer } from "gatsby-plugin-mdx"
5import Layout from "../components/layout"
6import components from "./mdxComponents"
8export default function PageTemplate({ data: { mdx } }) {
9 return (
10 <Layout>
11 <div style={{ padding: "0 1rem", marginBottom: "10rem" }}>
12 <h1>{mdx.frontmatter.title}</h1>
13 <h4
14 style={{
15 color: "gray",
16 fontWeight: "normal",
17 }}
18 >{`${mdx.frontmatter.date} by ${mdx.frontmatter.author}`}</h4>
19 <MDXProvider components={components}>
20 <MDXRenderer>{mdx.body}</MDXRenderer>
21 </MDXProvider>
22 </div>
23 </Layout>
24 )
27export const pageQuery = graphql`
28 query BlogPostQuery($id: String) {
29 mdx(id: { eq: $id }) {
30 id
31 body
32 frontmatter {
33 title
34 date
35 author
36 }
37 }
38 }

There are a few things to make note of here. If you are familiar with Gatsby and React, nothing should look too out of the ordinary here. Starting on line 27, we are using a graphQL query called BlogPostQuery to query our filesystem for an MDX file with the matching id. The id is generated in our createPages function and is passed via the context parameter to our page template component. From the results, we get our body and frontmatter data for the blog post from our MDX file. We can then access the query data in our component via our component's props (see line 8 above).

Styling MDX content

If we wanted to render the body and frontmatter data as plain HTML elements within our JSX, we could go ahead and do that within our PageTemplate component above. In fact, that is how the title is being rendered on line 12. But @mdx-js/react and gatsby-plugin-mdx provide us with <MDXProvider /> and <MDXRenderer /> components. These components allow us to customize the style of our MDX content. Let's take a look at how.

In the above code block, we're importing components from './mdxComponents' and passing it to <MDXProvider />. This components prop on <MDXProvider /> allows us to pass customized components for each type of Markdown element we may expect to render.

For example, in Markdown and MDX, a standard line of text gets rendered as a paragraph (<p>) element. But for my posts, I want my paragraph elements to contain a certain line height different from the default. I also want to provide my own margin and width properties. The way this can be accomplished is by creating a custom paragraph MDX component and passing it into the components prop to MDXProvider />.

In my src/components/mdxComponents directory, I have a paragraph.js file that looks as follows:

1import React from "react"
3export default ({ children }) => (
4 <p style={{ lineHeight: "1.7", maxWidth: "750px", margin: "30px auto" }}>
5 {children}
6 </p>

In the same src/components/mdxComponents directory, I also have an index.js file which exports the component from paragraph.js as well as all the other elements I have created customized MDX components for. I then import that as an object named components and pass it to <MDXProvider />. The body of our MDX file data is then passed as the children prop to <MDXRenderer /> on line 20, which then makes use of these components when it renders the MDX content. In addition to the paragraph element, a full list of other elements that can be customized in MDX can be found here.

Wrapping up

That concludes the process for how I've built out a blog feature, as well as the very blog post you are reading. Using Gatsby plugins gatsby-source-filesystem and gatsby-plugin-mdx, along with Gatsby's createPages API, we are able to use MDX files to dynamically generate pages for blog posts. Going forward, for my next post, all I will need to do is write the new MDX file and add it to src/posts/. I eventually plan on adding additional features such as tags, categories, search, and who knows what else. So if you are interested in that as well, stay tuned!

If you are interested in viewing the source code for the project, the repository for my site is publicly available on Github.

If you enjoyed this post or found it useful, please consider sharing it on Twitter.

If you want to stay updated on new posts, follow me on Twitter.

If you have any questions, comments, or just want to say hello, send me a message.

Thanks for reading!

← Back to posts