How to Build a Developer Blog with Gatsby and MDX – SitePoint

By Pro Web Design

2021-03-29 17:00:45

You can easily publish your ideas to sites like Dev.to, 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>WheeeeeeeeRainbowText>

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:


mkdir my-gatsby-blog

cd my-gatsby-blog

yarn init -y

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:


touch .gitignore

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

mkdir -p src/pages

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:



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:


git add .

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:


mkdir -p content/2021/03/06/hello-world,07/second-post,08/third-post

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:


git add .

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.

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: [
    
    
      
    ,
  ],
;

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: [
    
    
      
    ,
  ],
;

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": 

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:


mkdir src/hooks

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'

Styling with Theme UI

To style this project, I’m going to be using Theme UI, because of its speed with implementing layouts and features like dark mode. I’ll be detailing what’s relevant to what I’m doing and reasons for that, although this won’t be a guide on how to use Theme UI.

There’s a few additional dependencies to add for Theme UI, which are:

yarn add theme-ui gatsby-plugin-theme-ui @theme-ui/presets

With those installed, I’ll need to add the gatsby-plugin-theme-ui to the gatsby-config.js plugin array:

module.exports = {
  siteMetadata,
  plugins: [
    `gatsby-plugin-theme-ui`,
    `gatsby-plugin-mdx`,
    {
      resolve: `gatsby-source-filesystem`,
      

Now, if I stop and restart the dev server I have a slightly different looking site! It’s all gone a bit blue — or periwinkle, to be precise! This is the gatsby-plugin-theme-ui doing its thing and that color is the default.

The Gatsby plugin for Theme UI offers a lot of configuration options, some of which I’ll cover in more detail when needed. For now, I’m going to create a folder and define a theme object for Theme UI to use:


mkdir src/gatsby-plugin-theme-ui

touch src/gatsby-plugin-theme-ui/index.js

In the src/gatsby-plugin-theme-ui/index.js file, I’m going to add in a couple of the Theme UI presets, define the theme object, and spread in the swiss preset to the theme, to the theme colors, and to the styles.

For dark mode, I’m using the deep Theme UI preset and spreading that into the modes object for dark. (More on this soon.) For now, know that this is going to take care of a lot of the theming for me:

import  deep, swiss  from "@theme-ui/presets";

const theme = 
  ...swiss,
  colors: 
    ...swiss.colors,
    modes: 
      dark: 
        ...deep.colors,
      ,
    ,
  ,

  styles: 
    ...swiss.styles,
    p: 
      fontFamily: "body",
      fontWeight: "body",
      lineHeight: "body",
      fontSize: 3,
    ,
  ,
;

export default theme;

Now if I restart the dev server (again, yes, you’ll learn to deal with it) it will look a bit more acceptable with the Swiss theme being applied. At the time of writing, Theme UI sometimes doesn’t refresh the localhost page, so it’s necessary to do a browser page refresh.

Commit the changes so far to Git:

git add .
git commit -m 'add Theme UI and configure presets'

Time to add some React components!

Layout Component

Gatsby doesn’t have a specific layout, giving that responsibility to the developer. In this case, I’m making a layout for the whole site. It’s possible to incorporate many layouts for use in a Gatsby project, but for this example I’ll be using just one.

Now I’m going to refactor what I have currently so that everything is wrapped by a Layout component. What I have currently in src/pages/index.js can be used for a Header component, so I’m going to make a couple of files now for Layout and Header:


mkdir src/components

touch src/components/header.js src/components/layout.js

Now to move the title and description from src/pages/index.js to the newly created src/components/header.js component.

Rather than have the useSiteMetadata used in the Header component, I’ll pass the useSiteMetadata props I need to the header from the Layout component, which is where the header is going to live. (More on that shortly.) First up, here’s the header component, which lives in src/components/header.js:

import  Link as GatsbyLink  from "gatsby";
import React from "react";
import  Box, Heading, Link  from "theme-ui";

export const Header = ( siteTitle, siteDescription ) => 
  return (
    <Box as="header" sx= bg: "highlight", mb: "1.45rem" >
      <Box
        as="div"
        sx=
          m: "0 auto",
          maxWidth: "640px",
          p: "1.45rem 1.0875rem",
        
      >
        <Link as=GatsbyLink to="https://www.sitepoint.com/">
          <Heading>siteTitle</Heading>
        </Link>
        <Box as="p" variant="styles.p">
          siteDescription
        </Box>
      </Box>
    </Box>
  );
;

I’ve added in some basic styles using the Theme UI layout elements. This looks a bit different from before: Box, Link, Heading … what? These are all Theme UI components that can be used for layouts, form elements and more.

You may notice the as=GatsbyLink link prop added to the Link component. This uses the as prop in Theme UI and lets the component being passed in take on Theme UI styles.

There’s a great post from Paul Scanlon explaining in more detail how this is done in Theme UI. For a really comprehensive explanation of Theme UI, there’s also “Understanding Theme UI” by the same author.

There’s also the sx and variant props from Theme UI. sx enables additional styles to be passed to the component. Think of it as an equivalent to the JSX style= prop. The variant prop allows a group of predefined styles to be applied from the theme to the component being used.

Now for the Layout component, which is located in src/components/layout.js:

import React from "react";
import  Box  from "theme-ui";
import  useSiteMetadata  from "../hooks/use-site-metadata";
import  Header  from "./header";

export const Layout = ( children ) => 
  const  title, description  = useSiteMetadata();
  return (
    <>
      <Header siteTitle=title siteDescription=description />
      <Box
        as="div"
        sx=
          margin: "0 auto",
          maxWidth: "640px",
          padding: "0 1.0875rem 1.45rem",
        
      >
        <Box as="main">children</Box>
      </Box>
    </>
  );
;

Here I’m keeping the useSiteMetadata hook and passing the props the Header component needs, again with the sx prop to add some basic styles for alignment to the main containing div. Then I’m creating a main wrapper for the children.

The children prop is to return anything the Layout component encapsulates, which will include anything I want to apply the layout to. For example:

<Layout>
  <h1>This is wrapped</h1>
</Layout>

This will return everything in the Layout component and what it’s wrapping. In in the example above, that will currently be the header and the H1 wrapped by the Layout component.

As an example, I’ll go back to the index page (src/pages.index.js) and add the following:

import React from "react";
import  Layout  from "../components/layout";

export default function IndexPage() 
  return (
    <>
      <Layout>
        <h1>This is wrapped</h1>
      </Layout>
    </>
  );

The result is the header, provided in the Layout component and the H1 This is wrapped.

Index Page Posts Query

Now it’s time to get the posts I created at the beginning and display them on the index page as a list of clickable links.

To get the post information, I’ll recreate the query I made in the section on querying local files with GraphQL with a couple of extra bits:


  allMdx(sort:  fields: [frontmatter___date], order: DESC ) 
    nodes 
      id
      slug
      excerpt(pruneLength: 250)
      frontmatter 
        title
        date(formatString: "YYYY MMMM Do")
      
    
  

I’ve added in the id of the node and the slug. This is the file path to the .mdx files.

The excerpt is using a Gatsby function to get the first 250 characters from the post body, also adding some formatting to the date with another built-in Gatsby function.

Then as a way to order the posts in date descending order, I’ve added a sort: allMdx(sort: fields: [frontmatter___date], order: DESC ) {. This is sorting on the date in the posts front matter.

Adding that to the GraphiQL explorer gives me this result:

{
  "data": 
    "allMdx": 
      "nodes": [
        
          "id": "2bed526a-e5a9-5a00-b9c0-0e33beafdbcf",
          "slug": "2021/03/08/third-post/",
          "excerpt": "This is my third post! with a block quote! And a code block:",
          "frontmatter": 
            "title": "Third Post!",
            "date": "2021 March 8th"
          
        ,
        
          "id": "89ea266b-c981-5d6e-87ef-aa529e98946e",
          "slug": "2021/03/07/second-post/",
          "excerpt": "This is my second post!",
          "frontmatter": 
            "title": "Second Post!",
            "date": "2021 March 7th"
          
        ,
        
          "id": "75391ba1-3d6b-539f-86d2-d0e6b4104806",
          "slug": "2021/03/06/hello-world/",
          "excerpt": "My first post!! h2 Heading Some meaningful prose h3 Heading Some other meaningful prose",
          "frontmatter": 
            "title": "Hello World - from mdx!",
            "date": "2021 March 6th"
          
        
      ]
    
  ,
  "extensions": 
}

Now I can use that query in the src/pages/index.js file to get that data for use in the index page. In the IndexPage function, I’ll destructure data from the props given to the component via the GraphQL query:

import  graphql, Link as GatsbyLink  from "gatsby";
import React from "react";
import  Box, Heading, Link  from "theme-ui";
import  Layout  from "../components/layout";

export default function IndexPage( data ) 
  return (
    <>
      <Layout>
        data.allMdx.nodes.map(( id, excerpt, frontmatter, slug ) => (
          <Box
            key=id
            as="article"
            sx=
              mb: 4,
              p: 3,
              boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1)",
              border: "1px solid #d1d1d1",
              borderRadius: "15px",
            
          >
            <Link as=GatsbyLink to=`/$slug`>
              <Heading>frontmatter.title</Heading>
              <Box as="p" variant="styles.p">
                frontmatter.date
              </Box>
              <Box as="p" variant="styles.p">
                excerpt
              </Box>
            </Link>
          </Box>
        ))
      </Layout>
    </>
  );


export const query = graphql`
  query SITE_INDEX_QUERY 
    allMdx(sort:  fields: [frontmatter___date], order: DESC ) 
      nodes 
        id
        excerpt(pruneLength: 250)
        frontmatter 
          title
          date(formatString: "YYYY MMMM Do")
        
        slug
      
    
  
`;

This uses the components previously detailed. Note that the excerpt, frontmatter, and slug are being destructured from data.allMdx.nodes:

{data.allMdx.nodes.map(( excerpt, frontmatter, slug ) => (

Clicking on the links will take me to the Gatsby.js development 404 page. That’s because I haven’t made the pages for the .mxd files yet. That’s next.

I’ll commit what I’ve done so far before moving on:

git add .
git commit -m 'add Header and Layout components'

Using the Gatsby File System Route API with MDX

I’m going to be using the Gatsby File System Route API to get the file paths of the posts I created earlier on. The File System Route API is a way to programmatically create pages from my GraphQL data.

This approach has a special file notation for the page that’s going to be targeted when Gatsby generates the file system data at build time. The file indicates the node and the slug. I’ll create the file first, then detail where the data is coming from:


touch src/pages/mdx.slug.js

In the file, I’ll define a GraphQL query for the data I want to include in this template:

import  graphql  from "gatsby";
import  MDXRenderer  from "gatsby-plugin-mdx";
import React from "react";
import  Box  from "theme-ui";

export default function PostPage( data ) 
  const 
    body,
    frontmatter:  title ,
   = data.mdx;
  return (
    <>
      <Box as="h1" variant="styles.h1" fontSize="4xl">
        title
      </Box>
      <MDXRenderer>body</MDXRenderer>
    </>
  );


export const query = graphql`
  query POST_BY_SLUG($slug: String) 
    mdx(slug:  eq: $slug ) 
      id
      slug
      body
      frontmatter 
        date
        title
      
    
  
`;

Now that’s a lot of code, so I’ll break it down. It’s mainly to do with the GraphQL query:

query POST_BY_SLUG($slug: String) 
  mdx(slug:  eq: $slug ) 
    id
    slug
    body
    frontmatter 
      date
      title
    
  

The start of the query is taking in a slug with POST_BY_SLUG($slug: String), and the main node is mdx, so I’m using mdx.slug like the filename mdx.slug.js.

If I take that query and paste it into my GraphiQL explorer and press the play button, I get this:


  "data": 
    "mdx": null
  ,
  "extensions": 

That’s because there’s no variable defined for $slug in the GraphiQL explorer. If you look to the bottom of the query panel, you’ll see there’s a Query Variables section. Clicking this will expand it. In here is where I need to add a variable for slug. I’ll define it in curly braces with the path of one of the files:


  "slug": "2021/03/08/third-post/"

Running the query again, I’ll get all the data for that file. I’ve commented out the body output for readability:


  "data": 
    "mdx": 
      "id": "105a5c78-6a36-56e8-976c-d53d8e6ca623",
      "slug": "2021/01/08/third-post/",
      "body": "function _extends() ...", 
      "frontmatter": 
        "date": "2021-03-08T00:00:00.000Z",
        "title": "Third Post!"
      
    
  ,
  "extensions": 

What the File System Route API is doing is passing the individual file paths into the page query in src/pages/mdx.slug.js and returning the data to the page from that query in the ( data ) prop being passed to the page.

In this file, you may notice I’ve destructured the body from the data being returned, and then title from from the frontmatter, in a two-level destructure:

const 
  body,
  frontmatter:  title ,
 = data.mdx;

An alternative way to do it would be:

const body = data.mdx.body;
const title = data.mdx.frontmatter.title;

Using destructuring makes it a lot less verbose.

One last thing to note is the MDXRenderer wrapping the body of the post. This is everything included in the .mdx file after the front matter block. The compiled MDX from the GraphiQL query, which was commented out, is what needs to be wrapped in the MDXRenderer:

<MDXRenderer>body</MDXRenderer>

I’ll commit the changes now:

git add .
git commit -m 'create file route API file'

Root Wrapper Concept

Now clicking on one of the links on the index page will take me to the desired .mdx page, but it looks a bit different from the index page, right?

That’s because there’s no layout wrapping it yet. This is where I can use the Gatsby browser API and use the wrapPageElement function to wrap all the page elements. It’s also recommended that I use the same function in Gatsby SSR.

To avoid duplicating the same code in two files, I’ll create a third file with the actual code I’m going to use and import that into the two gatsby-* files mentioned.

First up, I’ll create the files needed:


touch gatsby-browser.js gatsby-ssr.js root-wrapper.js

The root wrapper file is where I’ll be using the wrapPageElement function:


import React from "react";
import  Layout  from "./src/components/layout";

export const rootWrapper = ( element ) => 
  return <Layout>element</Layout>;
;

Then, in both the gatsby-browser.js and gatsby-ssr.js files, I’ll add this:

import  rootWrapper  from "./root-wrapper";

export const wrapPageElement = rootWrapper;

If there are any changes needed to the wrapPageElement function, I can do it in the one file root-wrapper.js.

Time to stop and restart the dev server again to see the changes take effect!

Because the layout component is being used here to wrap all the page elements on the site, there’s no need to keep it on the index page anymore, so I’m going to remove that from src/pages/index.js:

import  graphql, Link as GatsbyLink  from "gatsby";
import React from "react";
import  Box, Heading, Link  from "theme-ui";
- import  Layout  from "../components/layout";

export default function IndexPage( data ) 
  return (
    <>
-      
        data.allMdx.nodes.map(( id, excerpt, frontmatter, slug ) => (
          
            
              frontmatter.title
              
                frontmatter.date
              
              
                excerpt
              
            
          
        ))
-      
    
  );
;
// rest unchanged

I’ll commit the changes so far before moving on:

git add .
git commit -m 'add root wrapper to Gatsby Browser and SSR'

404 Page

Time to make that 404 page!


touch src/pages/404.js

In the src/pages/404.js file, I’ll and add a message:

import React from "react";
import  Box, Heading  from "theme-ui";

export default function NotFound() 
  return (
    <>
      <Heading variant="styles.h1">
        Page not found!
        <span role="img" aria-label="crying face">
          😢
        </span>
      </Heading>
      <Box as="h2" variant="styles.h2">
        It looks like that page doesn't exist
      </Box>
    </>
  );

Now I can directly navigate to the 404 page to check it out: http://localhost:8000/404.

Note that, when developing using gatsby develop, Gatsby will continue to use the default 404 page that overrides your custom 404 page.

Commit this and move on to the next part:

git add .
git commit -m 'add 404 page'

Dark Theme Toggle

Dark mode is an essential feature of coding blogs. (I’m saying that half jokingly, in case you weren’t sure!) I’m going to use the Theme UI color mode hook useColorMode and do a simple toggle between the two modes I defined in the theme object earlier. Here’s what’s getting added to src/components/header.js:

import  Link as GatsbyLink  from "gatsby";
import React from "react";
+ import  Box, Button, Heading, Link, useColorMode  from "theme-ui";

export const Header = ( siteTitle, siteDescription ) => 
+  const [colorMode, setColorMode] = useColorMode();
  return (
    
      
        
          siteTitle
        
        
          siteDescription
        
+        
      
    
  );
;

But that doesn’t look great, so I’ll wrap the container with the Theme UI Flex component and shift the button over to the right:

import  Link as GatsbyLink  from "gatsby";
import React from "react";
+import  Box, Button, Flex, Heading, Link, useColorMode  from "theme-ui";

export const Header = ( siteTitle, siteDescription ) => 
  const [colorMode, setColorMode] = useColorMode();
  return (
    
      
+        
+          
            
              siteTitle
            
            
              siteDescription
            
+          
          
+        
      
    
  );
;

Git commit before moving to the next section:

git add .
git commit -m 'add theme toggle to header'

Code Blocks

The code blocks look a bit meh at the moment, so I’m going to add in some syntax highlighting with one of the many handy-dandy Theme UI packages. The one I’m using for this is Prism.

I’ll need to install the package and create a component in the gatsby-plugin-theme-ui folder called components.js:


yarn add @theme-ui/prism

touch src/gatsby-plugin-theme-ui/components.js

In that file, I’ll need to define where I want to apply the Prism styles to, which is all pre and code tags:

import Prism from "@theme-ui/prism";

export default 
  pre: (props) => props.children,
  code: Prism,
;

With that defined, I’ll also need to define in the theme object which Prism theme I want to use:

// scr/gatsby-plugin-theme-ui/index.js

import  deep, swiss  from "@theme-ui/presets";
+ import nightOwl from "@theme-ui/prism/presets/night-owl.json";

const theme = {
  ...swiss,
  colors: 
    ...swiss.colors,
    modes: 
      dark: 
        ...deep.colors,
      ,
    ,
  ,

  styles: {
    ...swiss.styles,
+    code: 
+      ...nightOwl,
+    ,
    // remainder of the file unchanged

Another stop and start of the dev server is needed to see the changes take effect!

Commit the changes and move onto the next section:

git add .
git commit -m 'add Prism package and update theme object'

Add Components to the MDX

This next bit is ptional. Markdown JSX allows React (JSX) components to be included in the Markdown. To demonstrate this, I’m going to add a RainbowText component that will animate some colors on an animation cycle. There’s an additional dependency I need for the animation: keyframes from @emotion/react. I’ll install that now:


touch src/components/rainbow-text.js

yarn add @emotion/react

This will probably trash the dev server if it’s running, so I’ll stop it for now.

In the src/components/rainbow-text.js file, I’ll be adding this component:

import  keyframes  from "@emotion/react";
import React from "react";
import  Box  from "theme-ui";

export const RainbowText = ( children ) => {
  const rainbow = keyframes(
    "0%": 
      backgroundPosition: "0 0",
    ,
    "50%": 
      backgroundPosition: "400% 0",
    ,
    "100%": 
      backgroundPosition: "0 0",
    ,
  );

  return (
    <Box
      as="span"
      variant="styles.p"
      sx=
        fontWeight: "heading",
        cursor: "pointer",
        textDecoration: "underline",
        ":hover": 
          background:
            "linear-gradient(90deg, #ff0000, #ffa500, #ffff00, #008000, #0000ff, #4b0082, #ee82ee) 0% 0% / 400%",
          animationDuration: "10s",
          animationTimingFunction: "ease-in-out",
          animationIterationCount: "infinite",
          animationName: `$rainbow`,
          WebkitBackgroundClip: "text",
          WebkitTextFillColor: "transparent",
        ,
      
    >
      children
    </Box>
  );
};

As this is optional, I won’t be going into detail on what’s going on here. Just know that it’s a nice CSS effect to have on hover.

With that component created, I can import it into any .mdx file I want to use it in. In this example, I’m adding it to content/2021/03/third-post/index.mdx. Here’s the diff of the file now that I’ve added the component:

---
title: Third Post!
date: 2021-03-08
---

+ import  RainbowText  from "../../../../../src/components/rainbow-text";

This is my third post!

> with a block quote!

+ Wheeeeeeee

And a code block:

```js
const wheeeeee = true;
```

After starting up the dev server again, I can go to the post where that component has been added, and when I hover over the text being wrapped in Wheeeeeeee, I can see that animation in effect.

You’ll probably be grimacing at that import: ../../../. On and on! There’s a way to go around this, however, using the root wrapper concept I detailed earlier and using the MDXProvider which will — ahem! — provide MDX with any components you pass to it.

Going back to the root wrapper (root-wrapper.js), I can wrap the page element with the MDXProvider and pass the RainbowText component to the MDXProvider:

import  MDXProvider  from "@mdx-js/react";
import React from "react";
import  Layout  from "./src/components/layout";
import  RainbowText  from "./src/components/rainbow-text";

const MDXComponents = 
  RainbowText,
;

export const rootWrapper = ( element ) => 
  return (
    <Layout>
      <MDXProvider components=MDXComponents>element</MDXProvider>
    </Layout>
  );
;

Now I can remove the import from the .mdx file:

---
title: Third Post!
date: 2021-03-08
---

- import  RainbowText  from "../../../../../src/components/rainbow-text";

This is my third post!

> with a block quote!

Wheeeeeeee

And a code block:

```js
const wheeeeee = true;
```

After stopping and restarting the dev server, I can go to this post and still see the RainbowText working. The extra advantage of adding components directly to the MDXProvider is that there’s no need to import a component into the .mdx document when you want to use it. It’s available via the provider for all MDX documents.

I’ll commit this now:

git add .
git commit -m 'add component for mdx'

Markdown Images

If I want to add images to my blog posts, I can include them in the MDX files, something like this:

---
title: Hello World - from mdx!
date: 2021-03-06
---

My first post!!

## h2 Heading

![mdx logo](./mdx-logo.png)

Some meaningful prose

### h3 Heading

Some other meaningful prose

The ./mdx-logo.png is a file I’ve added to the content/2021/03/06/hello-world folder, and I’m referencing it as a relative file. That’s not it for this, though. If I go to the hello world post, the image being displayed is broken. I’m going to need to add gatsby-remark-images as a plugin to gatsby-plugin-mdx so it knows what to do with the image files:

yarn add gatsby-remark-images gatsby-plugin-sharp

I’ll then need to configure the plugins in gatsby-config.js:

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

module.exports = {
  siteMetadata,
  plugins: [
    `gatsby-plugin-theme-ui`,
+    `gatsby-plugin-sharp`,
+    
+      resolve: `gatsby-plugin-mdx`,
+      options: 
+        gatsbyRemarkPlugins: [
+          
+            resolve: `gatsby-remark-images`,
+            options: 
+              maxWidth: 640,
+            ,
+          ,
+        ],
+      ,
+    ,
+    
+      resolve: `gatsby-source-filesystem`,
+      options: 
+        path: `$__dirname/content/`,
+      ,
+    ,
    
      resolve: `gatsby-source-filesystem`,
      options: 
        path: `$__dirname/content`,
        name: `content`,
      ,
    ,
  ],
};

The additional gatsby-source-filesystem object is letting Gatsby know where to look for the images to be processed.

Commit this now:

git add .
git commit -m 'add and configure images'

SEO

SEO is quite important if I want to have my content found on the Internet by search engines, so I’ll need to add the relevant meta tags to my blog here. It can be quite an involved process defining all the relevant tags needed, so to save time, I’ve created a React SEO Component for use in Gatsby for generating all the meta tags needed.

I’m going to yarn add the component along with the dependencies needed for it to work:

yarn add react-seo-component react-helmet gatsby-plugin-react-helmet

I’ll need to add the gatsby-plugin-react-helmet to the gatsby-config.js plugin array:

module.exports = {
  siteMetadata,
  plugins: [
+   `gatsby-plugin-react-helmet`,
    `gatsby-plugin-theme-ui`,
    `gatsby-plugin-sharp`,
    {
  // rest unchanged

Then it’s a case of using the SEO component throughout the site where I need to have meta tags.

The component takes quite a few props, many of which are defined once throughout the site, so the best place to add these would be in the siteMetadata object. Then I can pull out what I need with the useSiteMetadata hook.

I’m going to add several more properties to the siteMetadata object:

const siteMetadata = 
  title: `My Gatsby Blog`,
  description: `This is my coding blog.`,
+  lastBuildDate: new Date(Date.now()).toISOString(),
+  siteUrl: `https://dummy-url-for-now.com`,
+  authorName: `Author McAuthorson`,
+  twitterUsername: `@authorOfPosts`,
+  siteLanguage: `en-GB`,
+  siteLocale: `en_gb`,
;

If you’re following along, you can change these as needed. The siteUrl can be a dummy URL for now. That’s to help with pointing to any images needed for use in Open Graph protocol, and it’s the image you see when sharing a post you have made on Twitter, Facebook, LinkedIn and Reddit, for example.

Now that those additional properties are on the siteMetadata object, I’ll need to be able to query them. Currently the useSiteMetadata hook only has title and description, so I’ll add the rest in now:

// src/hooks/use-site-metadata.js

import  graphql, useStaticQuery  from "gatsby";
export const useSiteMetadata = () => 
  const  site  = useStaticQuery(
    graphql`
      query SITE_METADATA_QUERY 
        site 
          siteMetadata 
            title
            description
+            lastBuildDate
+            siteUrl
+            authorName
+            twitterUsername
+            siteLanguage
+            siteLocale
          
        
      
    `
  );
  return site.siteMetadata;
;

I’ll add the SEO component to all the pages. First up, I’ll do the posts pages in the src/pages/mdx.slug.js page. This is one of the most involved, so I’ll dump out the difference here and detail what’s going on:

import  graphql  from "gatsby";
import  MDXRenderer  from "gatsby-plugin-mdx";
import React from "react";
+ import SEO from "react-seo-component";
import  Box  from "theme-ui";
+ import  useSiteMetadata  from "../hooks/use-site-metadata";

export default function PostPage( data ) 
  const 
    body,
+    slug,
+    excerpt,
+    frontmatter:  title, date ,
   = data.mdx;
+  const 
+    title: siteTitle,
+    siteUrl,
+    siteLanguage,
+    siteLocale,
+    twitterUsername,
+    authorName,
+   = useSiteMetadata();
  return (
    <>
+      
      
        title
      
      body
    
  );


export const query = graphql`
  query POST_BY_SLUG($slug: String) 
    mdx(slug:  eq: $slug ) 
      id
      slug
      body
+      excerpt
      frontmatter 
        date
        title
      
    
  
`;

The siteUrl, slug and excerpt are needed for the canonical link (very important in SEO) and the excerpt is for the meta description.

I’m using the siteMetadata hook to get the rest of the information the component needs. title and titleTemplate are used to make up what you see in the browser tab.

The article Boolean is for the component, so it can create the breadcrumb list in JSONLD format. The rest of the props are to help identify the author and published date. 😅

That was a lot. I hope some of it made sense! For the scope of this post, I’ll leave it there, but there’s a lot more to dig into on this subject, and I mean a lot!

Thankfully the src/pages/index.js page is a bit simpler!

import  graphql, Link as GatsbyLink  from "gatsby";
import React from "react";
+ import SEO from "react-seo-component";
import  Box, Heading, Link  from "theme-ui";
+ import  useSiteMetadata  from "../hooks/use-site-metadata";

export default function IndexPage( data ) {
+  const 
+    title,
+    description,
+    siteUrl,
+    siteLanguage,
+    siteLocale,
+    twitterUsername,
+   = useSiteMetadata();
  return (
    <>
+      
      {data.allMdx.nodes.map(( id, excerpt, frontmatter, slug ) => (
// rest of component unchanged

I’ve intentionally left out the image from both examples. If you’re interested in making your own Open Graph images to use in this component, check out the post “Open Graph Images with Gatsby and Vercel” for how to do this with a serverless function. 🔥

Now I can build the site (almost ready for production), and once it’s built I can check out the page source for the meta tags:


yarn build

yarn serve

Once the build has finished, I can use yarn serve to have the built site served locally on localhost:9000. In the browser, I can view the page source with the keyboard shortcut Ctrl + u. From here, I can check for the canonical meta tag, which will be the dummy URL used in the metadata.

Alrighty! Commit this to Git and move on:

git add .
git commit -m 'add SEO component :sweat_smile:'

Push It to GitHub

You may be wondering why I’ve been making Git commits at the end of each section. That’s because I’m going to push the project up to GitHub now.

I’ll log in to my GitHub account and select the plus + icon next to my avatar image on the top right corner and select New repository.

In the Repository name, I’ll add in the project name my-gatsby-blog but leave the rest of the defaults and click Create repository.

The next screen gives me the terminal commands I need to push my local project to GitHub:

git remote add origin https://github.com/spences10/my-gatsby-blog
git branch -M main
git push -u origin main

Once you’ve put all those into the terminal and hit Enter, refresh the GitHub page to see the new project!

Deploy

Time to put this baby on the Web! There are many ways to do this. Because Gatsby builds to a flat file structure, you can host a Gatsby site on any file server with access to the Internet.

There are many services out there that offer hosting on a CDN, many for free! Services like Netlify, Vercel and Render will allow you to push your built site to their CDNs via a CLI, GitHub integration, or, in the case of Netlify, a straight up drag and drop!

Vercel

To deploy with Vercel, you’ll need a GitHub, GitLab or Bitbucket account to authenticate with. Then you’ll be prompted to install the Vercel CLI:

yarn global add vercel

I already have it installed, so now it’s a case of running the CLI command:

vc

I’m then prompted to set up and deploy the new project. I’m going to answer the default to all the questions with Enter:

Set up and deploy “~/repos/my-gatsby-blog”? [Y/n]
Which scope do you want to deploy to?
Link to existing project? [y/N]
What’s your project’s name? (my-gatsby-blog)
In which directory is your code located? ./
> Upload [====================] 99% 0.0sAuto-detected Project Settings (Gatsby.js):
- Build Command: `npm run build` or `gatsby build`
- Output Directory: public
- Development Command: gatsby develop --port $PORT
? Want to override the settings? [y/N]

That’s it. I’m then given a deployment URL where I can watch the build of the site on Vercel.

From the Vercel dashboard I can configure the domain, and also buy one from Vercel if I want. I personally use Namecheap.com, but it’s an option.

Netlify

Deploying with Netlify via the CLI is much the same as with Vercel, but I’m going to do the drag-and-drop creation.

For authentication, I’ll need one of GitHub, GitLab, Bitbucket or email account. Once I’ve authenticated and logged in, I can select Sites in the menu bar, then there’s a drop area Want to deploy a new site without connecting to Git? Drag and drop your site output folder here. I’m going to navigate in my file explorer to the root of my project and drag and drop the public folder to the drop area.

Netlify will build the files and deploy them to a generated URL for inspection. Much the same as with Vercel, Netlify will let you purchase a domain there and deploy to it.

Render

Render doesn’t have a CLI or drop option and instead uses a GitHub integration. To authenticate, I’ll need a GitHub, GitLab or Google account. Once I’ve authenticated and logged in, I’m on the services section. From here, I can select New Static Site then enter my GitHub URL for the project I pushed to GitHub earlier.

On the next page, I’ll give it the following settings:

  • Name: my-gatsby-blog
  • Branch: the default value
  • Build command: yarn build
  • Publish directory: ./public

Then click Create Static Site.

Wait for Render to do its thing, and then click the link below the project name to see the site live.

Render also has the option to set your own custom domain for the site!

Optional Gatsby plugins

There are many more Gatsby plugins to choose from for adding additional functionality. I’ll leave these to you if you want to add more. For example:

Analytics

If you’re interested in knowing how popular your site is, there are analytics options. I stopped using Google Analytics a while back on my own projects, and I now prefer more privacy-focused alternatives. One I recommend is Fathom Analytics. (I have an affiliate link if you want to get $10 off your first month’s subscription.)

Another alternative is Plausible, which I’ve also heard good things about.

To implement Fathom Analytics on a Gatsby site, I’ll need to add an additional script tag to the head of my site. What does that mean? Well, first up I’ll need to create the site on my Fathom dashboard, then go to https://app.usefathom.com/#/settings/sites, scroll to the bottom of the list, add in my new site (my-gatsby-blog), then click Get site code. I then get a popup modal with the site code. I’ll need that for the script I’m going to add to the head of my Gatsby project. Here’s what the script looks like:

<script
  src="https://cdn.usefathom.com/script.js"
  data-spa="auto"
  data-site="ABCDEF"
  defer
></script>

Here’s the diff of root-wrapper.js:

import  MDXProvider  from "@mdx-js/react";
import React from "react";
+import  Helmet  from "react-helmet";
import Layout from "./src/components/layout";
import RainbowText from "./src/components/rainbow-text";

const MDXComponents = 
  RainbowText,
;

export const wrapPageElement = ( element ) => 
  return (
+    <>
+      
+        
+      
      
        element
      
+    
  );
;

Wrap!

That’s it from me. Thank you so much for making it to the end. 🙏

I hope you got what you needed from this quite extensive guide on setting up a Gatsby project from scratch!

If you want to reach out and say hi, the best place to get me is on Twitter.



#Build #Developer #Blog #Gatsby #MDX #SitePoint

Recent Posts