How to Build a Developer Blog with Gatsby and MDX

You can easily publish your ideas to sites like, Hashnode or Medium, but the ideal is to have full control over your own content. There’s an ever-growing list of tools for building your own website and controlling your own content. In this extensive tutorial, I’ll be covering how you can make your content shine using Gatsby, with the added bells and whistles you get with such an ecosystem.

I originally used Jekyll to publish my blog, but then switched to Gatsby, using the Lumen template. I’ve been using Gatsby since version 0, around May 2017.

I’ll be going from a Hello, World! Gatsby project through to a coding blog with code syntax highlighting and a theme toggle for that dark mode goodness.

There’s a rich ecosystem of plugins, starters and themes available for Gatsby to get you up and running quickly, but I want to take a progressive disclosure approach to presenting Gatsby, focusing on the basics of how a Gatsby project works.

Why Gatsby?

Gatsby is a static site generator, so there’s no dynamic generation of pages when the pages are requested. The built output for a Gatsby site can be hosted on a CDN, making it globally available and super scalable.

Gatsby can use Markdown files to create pages in a site project. Gatsby will read the Markdown files into the Gatsby file system and transform the Markdown to HTML and then when building the site create static pages.

The end result is a super fast site with little latency when requesting the pages.

Markdown and MDX

I’ve been documenting my development journey since 2016 in Markdown. Markdown offers a way to enable simple editing in plain text files that can be converted to HTML.

MDX (or Markdown JSX) is a tool that lets you write JSX in your Markdown documents, sort of like this:

import { RainbowText } from './components/rainbow'; ## A Markdown Heading <RainbowText>Wheeeeeeee</RainbowText> 

Gatsby is by far the best framework I’ve used for working with Markdown and MDX, as the there’s no special notation needed above using frontmatter on your posts.

What Do I need?

If you’re going to follow along, there’s a few things you’ll need:

  • a basic web development setup: Node, terminal (bash, zsh or fish)
  • a text editor
  • a basic understanding of React

If you don’t have any of these, there’s both StackBlitz and GitHub Codespaces where you can create an empty GitHub repository and get started with a development environment from there.

I’ll be using VS Code as my text editor and Yarn as my preferred package manager in the examples below. If you prefer npm, that’s cool. 👍

You can also find the complete code for this tutorial on GitHub.

Okay, it’s time to get started!

Hello, World!

It’s time to spin up a Gatsby project. I’m going to do the majority of this from the command line to begin with:

# create the project directory mkdir my-gatsby-blog # change into the directory cd my-gatsby-blog # initialise a package.json file yarn init -y # initialise the git repo git init 

Cool. Now, before going anywhere else with this, I’m going to need to add a .gitignore file before installing any npm modules:

# create .gitignore file in my directory touch .gitignore # add ignore contents with echo echo "# Project dependencies .cache node_modules # Build directory public # Other .DS_Store yarn-error.log" > .gitignore 

Now I can install all the npm goodness I need to without VS Code Git screaming at me about too many active changes. Let’s now install some dependencies to get up and running with Gatsby:

yarn add gatsby react react-dom # -p is to create parent directories too if needed mkdir -p src/pages # create the index (home) page touch src/pages/index.js 

Next, we’ll add the first React component (of many) for the project. I’ll add the following to the index.js file I created:

import React from "react"; export default function IndexPage() { return <h1>Hello, World!</h1>; } 

I’m now ready to run the Gatsby develop command from the command line:

# if you're using npm 👇 # $(npm bin)/gatsby develop yarn gatsby develop 

This will spin up the Gatsby dev sever and say that my project is available to view in the browser on port 8000 (the default Gatsby port). The URL is http://localhost:8000/.

Using the Gatsby binary commands directly from the command-line interface (CLI) is totally doable, but most people will add the available commands to the scripts section on the package.json file, like this:

"scripts": { "build": "gatsby build", "dev": "gatsby develop", "serve": "gatsby serve", "clean": "gatsby clean" }, 

As an added bonus, there’s a few extras that can be added to the Gatsby scripts here.

If we don’t want to run the project on the same port each time, it can be changed with the -p flag, and and a port specified after that. For example, gatsby develop -p 8945.

If we want to open the browser tab once the project is ready, we can add -o to the script.

I’ll do the same with the serve script, so I know when I’ve built a project it’s on a different port to the development one:

"scripts": { "build": "gatsby build", "dev": "gatsby develop -p 8945 -o", "serve": "gatsby serve -p 9854 -o", "clean": "gatsby clean" }, 

And with that, the mandatory “Hello, World!” welcome is complete and I can move on with the rest of this post! 🤓

Lastly I’ll commit the changes I’ve made so far:

# add everything for committing git add . # commit to repo git commit -m 'init project' 

Content for the Blog

Okay, there’s not a great deal going on with the project right now, so first up I’ll add in some content, from the command line again:

# this creates the folders in the root of the project mkdir -p content/2021/03/{06/hello-world,07/second-post,08/third-post} # create individual files touch content/2021/03/06/hello-world/index.mdx touch content/2021/03/07/second-post/index.mdx touch content/2021/03/08/third-post/index.mdx 

I’ll be using these throughout the examples I’m making.

You’ll notice the file extension .mdx. This is an MDX file.

Front matter

Before I add some content for the blog, I’ll need to talk about front matter.

Front matter is a way to store information about the file that can be used by Gatsby when building the pages from them. For now, I’ll add a title of the post and a date. I’ll also add some content to them. Here’s our first post:

--- title: Hello World - from mdx! date: 2021-03-06 --- My first post!! ## h2 Heading Some meaningful prose ### h3 Heading Some other meaningful prose 

Here’s our second post:

--- title: Second Post! date: 2021-03-07 --- This is my second post! 

A third post:

--- title: Third Post! date: 2021-03-08 --- This is my third post! > with a block quote! And a code block: ```js const wheeeeee = true; ``` 

That’s it for the posts for now, because these posts aren’t yet recognized by Gatsby as pages. I’ll need to let Gatsby know where to find content to add to the project. To do this, I’m going to add a configuration file to Gatsby.

Let’s commit the changes I’ve made to Git:

# add changed file for committing git add . # commit to repo git commit -m 'add markdown files' 

Gatsby Config

Gatsby config is what’s used to define and configure the many Gatsby plugins you can use. More on the Gatsby plugin eco system in a bit. For now, I’m going to create the file, again in the terminal:

touch gatsby-config.js 

This creates the gatsby-config.js at the root of the project so I can start configuring Gatsby to read the .mdx files I created earlier.

Gatsby Plugins

Now I can install and configure the plugins Gatsby needs to source and display the files I created. I’ll install them all now and briefly detail what they’re for:

yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react gatsby-source-filesystem 

A quick look at the package.json now shows that I have the following dependency version installed:

"dependencies": { "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", "gatsby": "^3.1.1", "gatsby-plugin-mdx": "^2.1.0", "gatsby-source-filesystem": "^3.1.0", "react": "^17.0.1", "react-dom": "^17.0.1" }, 

One thing to note is that, in Gatsby, there’s no need to import React in your components with React 17. But for the sake of completeness, and to avoid any confusion, I’ll be including it in these examples.

Now I need to configure gatsby-plugin-mdx and gatsby-plugin-mdx. In the gatsby-config.js file, I’ll add this:

module.exports = { plugins: [ `gatsby-plugin-mdx`, { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/content`, name: `content`, }, }, ], }; 

Commit changes up to now:

git add . git commit -m 'add gatsby plugins' 

Gatsby GraphQL

Now it’s time to see where I’m at with the files in Gatsby by using the Gatsby GraphQL client, GraphiQL. You may have noticed, if you’re following along, that the CLI indicates two URL locations to view the project:

You can now view my-gatsby-blog in the browser. ⠀ http://localhost:8000/ ⠀ View GraphiQL, an in-browser IDE, to explore your site's data and schema ⠀ http://localhost:8000/___graphql 

I’m going to be using the ___graphql (three underscores) route now to see the files in the file system.

If this seems a bit intimidating, I’ll attempt to cover all the parts that may not seem to make much sense. If you’re following along, you should be fine copying the examples into the GraphiQL explorer.

When I open up the GraphiQL explorer, I have several Explorer panels. This is all available data to explore in the project and is dependent on what I’ve configured in the gatsby-config.js file.

The GraphiQL query panel and the results are next to that. This is where I’ll be writing GraphQL queries to retrieve the data I need. There’s also a QUERY VARIABLES section at the bottom of the query panel, and I’ll come onto that later on.

Over on the far right is the GraphQL Documentation Explorer. Because of GraphQL’s strict typing, this means that it’s able to generate its own documentation on its data. But that’s outside the scope of this post.

Query Local Files with GraphQL

Next, I’m going to query for the files I added earlier in the GraphiQL query panel. In this query, I’m querying the title and date defined in the font matter of the files:

{ allMdx { nodes { frontmatter { title date } } } } 

If we pop that into the query panel press the big play button, we get back some data in the results panel. We can also use the Explorer in the left panel to pick out the data. Here’s what I get after running the query:

{ "data": { "allMdx": { "nodes": [ { "frontmatter": { "title": "Hello World - from mdx!", "date": "2021-03-06T00:00:00.000Z" } }, { "frontmatter": { "title": "Second Post!", "date": "2021-03-07T00:00:00.000Z" } }, { "frontmatter": { "title": "Third Post!", "date": "2021-03-08T00:00:00.000Z" } } ] } }, "extensions": {} } 

This is a big JSON object with the relevant information we requested in the query. We’ll look at how to use this soon. For now, this means that we can use this data in the Gatsby project to make pages.

Site Metadata

In the gatsby-config.js file, there’s also an option to specify site metadata. Site metadata is for when I want to reuse common data like the site title and description.

This is will be useful further down the road when I want to add meta tags to the site for search engine optimization (SEO). (Again, more on that later.) For now, I’m going to define some basic information about the site in the gatsby-config.js with the siteMetadata object.

I could define the site metada directly in the module.exports like so:

module.exports = { siteMetadata: { title: `My Gatsby Blog`, description: `This is my coding blog.`, }, plugins: [ // configured plugins here { // empty for brevity }, ], }; 

The site metadata object can get a bit large, and I’ve found keeping it in its own object can make it a bit simpler to reason about, so instead I’m going to define it separately:

const siteMetadata = { title: `My Gatsby Blog`, description: `This is my coding blog.`, }; 

Then add the siteMetadata object to the Gatsby config file:

const siteMetadata = { title: `My Gatsby Blog`, description: `This is my coding blog.`, }; module.exports = { siteMetadata, plugins: [ // configured plugins here { // empty for brevity }, ], }; 

Now I can hop over to the GraphiQL explorer again and query that site metadata with the following query:

{ site { siteMetadata { title description } } } 

It’s always a good idea to stop and restart the development server if you’re making changes to the gatsby-config.js file, so I’ll do that (Ctrl + c, then yarn develop), then in the GraphiQL explorer refresh the page and run the query again to get the data back:

{ "data": { "site": { "siteMetadata": { "title": "My Gatsby Blog", "description": "This is my coding blog." } } }, "extensions": {} } 

Make a Site Metadata Hook

Now that I have the site metadata in the Gatsby file system, I can query it wherever I want to use it with the Gatsby static query hook useStaticQuery. I’m going to kill off the dev server and restart after I’ve added the following to the src/pages/index.js file:

import { graphql, useStaticQuery } from "gatsby"; import React from "react"; export default function IndexPage() { const { site: { siteMetadata }, } = useStaticQuery(graphql` { site { siteMetadata { title description } } } `); console.log("====================="); console.log(siteMetadata); console.log("====================="); return <h1>Hello World!</h1>; } 

A quick note on some of the notation there: const { site: { siteMetadata }, } is quick way to get to the data in the site query, where I’m pulling the siteMetadata from the site object. This is referred to as destructuring.

Now, after I’ve started the dev server again, I can go over to the browser console (Control + Shift + J in Windows/Linux, Command + Option + J on macOS) and see the siteMetadata object in the console output.

I get the following console output:

===================== {title: "My Gatsby Blog", description: "This is my coding blog."} description: "This is my coding blog." title: "My Gatsby Blog" __proto__: Object ===================== 

Don’t worry about the console warning for a missing 404 page not found (net::ERR_ABORTED 404 (Not Found)). I’ll make that later.

To avoid having to write this query each time, I want to use it in a component. I’m going to abstract this out into its own hook:

# make a folder for all the hooks to live mkdir src/hooks # creathe the file touch src/hooks/use-site-metadata.js 

Now I’ll add in a hook to the newly created src/hooks/use-site-metadata.js file to get the site metadata on demand:

import { graphql, useStaticQuery } from "gatsby"; export const useSiteMetadata = () => { const { site } = useStaticQuery( graphql` query SITE_METADATA_QUERY { site { siteMetadata { title description } } } ` ); return site.siteMetadata; }; 

You may have noticed that this query isn’t the same as the one from from the GraphiQL explorer:

+ query SITE_METADATA_QUERY { site { siteMetadata { title description } } } 

This is to name the query. Because I’ll be using a lot of queries in the project, it makes sense to give them meaningful names.

Now I’ll implement the new hook into the src/pages/index.js file:

import React from "react"; import { useSiteMetadata } from "../hooks/use-site-metadata"; export default function IndexPage() { const { title, description } = useSiteMetadata(); return ( <> <h1>{title}</h1> <p>{description}</p> </> ); } 

That’s a lot less verbose, and I’m able to pick and choose what items I want from the SITE_METADATA_QUERY.

It’s time to commint the changes made so far:

git add . git commit -m 'add site metadata and metadata hook' 

Continue reading How to Build a Developer Blog with Gatsby and MDX on SitePoint.