How to Create Your Own Gatsby.js Theme Part One

March 25th 2020

This tutorial is segmented into two parts. Part One discusses setting up the Yarn Workspace that we will use for development purposes. We will setup our data source using Gatsby’s filesystem source plugin and transform JSON data using Gatsby’s transform json plugin. Part Two we will discuss extending the theme and using it as a package dependency. We will include styling options use Theme UI and discuss how we can override our theme styles and theme components using component shadowing.

Gatsby.js is a wonderful static site generator that is built on top of React. There are many large websites that use Gatsby as their front-end of choice because of the performance benefits of the platform. Gatsby really shines when it comes to speed and security since all of the pages are generated at build time and minified for the optimal page size and delivery.

A Gatsby theme is a module dependency (just like any other node dependency) which can be written to configure Gatsby and provide functionality to any project that wants to use (or extend) the theme. In this tutorial we’ll walk through the process of creating a Gatsby theme which you can publish and distribute for others to use.

Yarn Workspace Setup

Since we’ll be building out a Gatsby theme we will use yarn workspaces to create the theme and also setup a new Gatsby project that will depend on the theme in the other workspace. One benefit of yarn workspaces is that they can have a dependency on one another:

Your dependencies can be linked together, which means that your workspaces can depend on one another while always using the most up-to-date code available. This is also a better mechanism than yarn link since it only affects your workspace tree rather than your whole system.

https://classic.yarnpkg.com/en/docs/workspaces/

We’ll first create an empty folder that will hold both workspaces we’re about to create. Navigate to a place on your computer where you want to setup the project and run mkdir gatsby-theme-builder-tutorial which is our parent project folder. Change directories into the new folder we created and create a package.json file. You can open the package.json in your editor of choice and add:

{
  "private": true,
  "workspaces": ["gatsby-theme-portfolio", "test-site"]
}

We’re creating two workspaces here: gatsby-theme-portfolio and test-site in which we’ll use to develop our theme. The private: true ensures that we won’t publish the parent theme as we will only be publishing the gatsby-theme-portfolio project to npm. We will need to create the two folders for our workspaces: mkdir gatsby-theme-portfolio && mkdir test-site. Now we will have to create separate package.json files for each of these folders. We’ll start with the package.json file in the gatsby-theme-portfolio folder:

{
  "name": "gatsby-theme-portfolio",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "gatsby build",
    "clean": "gatsby clean",
    "develop": "gatsby develop"
  }
}

The "name" corresponds to the yarn workspace you defined earlier, in the root-level package.json file which was gatsby-theme-portfolio. As you can see we are required to create a "main": "index.js" file since this is a package that is going to be published to npm. This can be a blank file with some comments:

// silence

Now we can go over to our other workspace test-site and create a similar package.json file:

{
  "private": true,
  "name": "test-site",
  "version": "1.0.0",
  "license": "MIT",
  "scripts": {
    "build": "gatsby build",
    "develop": "gatsby develop",
    "clean": "gatsby clean"
  }
}

We are going to set "private": "true" in this package.json file as well since we won’t be publishing the test-site project but rather we’ll be using it to test our theme dependency. We can also add our dependency requirements to our test-site workspace. Change directories into the main root folder gatsby-theme-builder-tutorial and run:

yarn workspace test-site add gatsby react react-dom gatsby-theme-portfolio@*

We are creating a dependency for our test-site to our other workspace gatsby-theme-portfolio. We can verify this by running yarn workspaces info

{
  "gatsby-theme-portfolio": {
    "location": "gatsby-theme-portfolio",
    "workspaceDependencies": [],
    "mismatchedWorkspaceDependencies": []
  },
  "test-site": {
    "location": "test-site",
    "workspaceDependencies": [
      "gatsby-theme-portfolio"
    ],
    "mismatchedWorkspaceDependencies": []
  }
}

You can see under our “test-site” under “workspaceDependencies” we have a dependency on our other workspace! Since other projects will be consuming our gatsby-theme-portfolio and since they are gatsby projects they will need the same dependencies we just downloaded: gatsby react react-dom so we will need to add these as peerDependencies to our gatsby-theme-portfolio by running yarn workspace gatsby-theme-portfolio add -P gatsby react react-dom and also since we are going to be using this project to develop our Gatsby theme with we will need to install these as devDependencies too: yarn workspace gatsby-theme-portfolio add -D gatsby react react-dom. Our package.json file in our gatsby-theme-portfolio should look like:

{
  "name": "gatsby-theme-portfolio",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "gatsby build",
    "clean": "gatsby clean",
    "develop": "gatsby develop"
  },
  "peerDependencies": {
    "gatsby": "^2.20.4",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "devDependencies": {
    "gatsby": "^2.20.4",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }
}

Now with the setup in place we can run both the test-site and the gatsby-theme-portfolio projects to make sure they are working as expected:

yarn workspace test-site run develop
yarn workspace gatsby-theme-portfolio run develop

With both of those working we can move on to the fun part building out the theme!

Developing Your Gatsby Theme

In this tutorial we will be using gatsby-source-filesystem plugin to source the files in the project into nodes and we will also be using the transformer plugin gatsby-transformer-json plugin to transform .json files into GraphQL nodes as well. Let’s install these dependencies:

yarn workspace gatsby-theme-portfolio add gatsby-source-filesystem gatsby-transformer-json

Now with those installed we can create a gatsby-config.js file inside of our gatsby-theme-portfolio folder to define our plugins:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `./src/data`,
      },
    },
     {
      resolve: `gatsby-transformer-json`,
      options: {
        typeName: ({ node }) => node.name
      },
    },
  ],
}

In this file we are setting some configuration options. The first option in gatsby-source-filesystem is the path that we want our data files sourced from which is going to be src/data folder (which we will create in a bit). The second option in gatsby-transformer-json is the typeName which will be used for our GraphQL Type definitions. We are using the node.name which is going to be the name of the .json file we create. For instance, if we create a file that will hold all of our projects called: src/data/Project.json the GraphQL type will be Project and in our GraphQL query we can use the GraphQL query:

query{
  allProject{}
}

And this will return all of our projects which are defined in the Project.json file. Before we forget let’s create the folders: src/data in our gatsby-theme-portfolio workspace and we can add the file Project.json. Since this will be a list of projects we can define the file as such:

[
  {
    "name": "My Gatsby Project",
    "url": "https://github.com/hashinteractive/gatsby-theme-reactor/tree/master/gatsby-theme-reactor",
    "technologies": ["react.js", "gatsby.js", "GraphQL", "Tailwind CSS"]
  },
  {
    "name": "My Second Project",
    "url": "https://github.com/hashinteractive/gatsby-theme-reactor/tree/master/gatsby-theme-reactor",
    "technologies": ["vue.js", "nuxt.js", "GraphQL", "Tailwind CSS"]
  }
]

Now that we have those defined we can run gatsby develop in our workspace:

yarn workspace gatsby-theme-portfolio run develop

And we can open up or GraphiQL explorer and run:

{
  allProject {
    edges {
      node {
        name
        technologies
        url
      }
    }
  }
}

And we should see

{
  "data": {
    "allProject": {
      "edges": [
        {
          "node": {
            "name": "My Gatsby Project",
            "technologies": [
              "react.js",
              "gatsby.js",
              "GraphQL",
              "Tailwind CSS"
            ],
            "url": "https://github.com/hashinteractive/gatsby-theme-reactor/tree/master/gatsby-theme-reactor"
          }
        },
        {
          "node": {
            "name": "My Second Project",
            "technologies": [
              "vue.js",
              "nuxt.js",
              "GraphQL",
              "Tailwind CSS"
            ],
            "url": "https://github.com/hashinteractive/gatsby-theme-reactor/tree/master/gatsby-theme-reactor"
          }
        }
      ]
    }
  }
}

Working as expected! Great, now we have some data to work with. But we will need to take into consideration that the sites using our theme might not have the src/data/Project.json folder structure so let’s run some test to make sure they do!

Create a data directory using the onPreBootstrap lifecycle

We can use the the lifecycle api onPreBootstrap to make sure our folder structure is correct. We can run this hook in gatsby-node.js file. Let’s create that file in our gatsby-theme-portfolio project folder:

const fs = require("fs")
// Make sure the src/data directory exists
exports.onPreBootstrap = ({ reporter }) => {
  const dataPath = "src/data"
  if (!fs.existsSync(dataPath)) {
    reporter.info(`creating the ${dataPath} directory`)
    fs.mkdirSync(dataPath)
  }
}

We’re requiring the fs node package to create the folder structure src/data if it does not exist.

Manipulating GraphQL Type Definitions

Since we know we have a GrapQL Type Project we can manipulate the type definition (which is created by gatsby-tranformer-json plugin to add fields to our type definition. The first field we’re going to add is the slug field which we will define later as a custom resolver. We can do this by hooking into the sourceNodes api in which we can use the createTypes action to manipulate the Project type. Open up gatsby-theme-portfolio/gatsby-node.js and add a new line:

const fs = require("fs")
// Make sure the src/data directory exists
exports.onPreBootstrap = ({ reporter }) => {
  const dataPath = "src/data"
  if (!fs.existsSync(dataPath)) {
    reporter.info(`creating the ${dataPath} directory`)
    fs.mkdirSync(dataPath)
  }
}

exports.sourceNodes = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type Project implements Node {
      slug: String! 
    }
  `
  createTypes(typeDefs)
}

We will allow Gatsby to automatically infer type definitions for the other fields in our Project.json file, but the slug field we can define in addition to the other fields. Now that we have it defined as type String! we will need to create a custom resolver to resolve the field value. We can do this using Gatsby’s createResolvers API.

We are going to use the createResolver API to resolve our slugify field which we will pass the field name of the Project and slugify it with this handy helper function you can find on Medium:

function slugify(string) {
  const a = 'àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;'
  const b = 'aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------'
  const p = new RegExp(a.split('').join('|'), 'g')

  return string.toString().toLowerCase()
    .replace(/\s+/g, '-') // Replace spaces with -
    .replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters
    .replace(/&/g, '-and-') // Replace & with 'and'
    .replace(/[^\w\-]+/g, '') // Remove all non-word characters
    .replace(/\-\-+/g, '-') // Replace multiple - with single -
    .replace(/^-+/, '') // Trim - from start of text
    .replace(/-+$/, '') // Trim - from end of text
}

Now back to our gatsby-node.js file for our createResolvers definition. We can add our definition in the exports.createResolvers lifecycle api that gatsby provides in our gatsby-node.js file:

const fs = require("fs")
// Make sure the src/data directory exists
exports.onPreBootstrap = ({ reporter }) => {
  const dataPath = "src/data"
  if (!fs.existsSync(dataPath)) {
    reporter.info(`creating the ${dataPath} directory`)
    fs.mkdirSync(dataPath)
  }
}

exports.sourceNodes = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type Project implements Node {
      slug: String! 
    }
  `
  createTypes(typeDefs)
}

exports.createResolvers = ({ createResolvers }) => {
  const resolveSlug = (source) => {
    const a = 'àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;'
    const b = 'aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------'
    const p = new RegExp(a.split('').join('|'), 'g')

    const slugify = string => string.toString().toLowerCase()
      .replace(/\s+/g, '-')
      .replace(p, c => b.charAt(a.indexOf(c)))
      .replace(/&/g, '-and-')
      .replace(/[^\w\-]+/g, '')
      .replace(/\-\-+/g, '-')
      .replace(/^-+/, '')
      .replace(/-+$/, '')

    const slug = slugify(source.name)
    const basename = slugify(source.internal.type)

    return `/${basename}/${slug}`
  }

  createResolvers({
    Project: {
      slug: {
        resolve: source => resolveSlug(source),
      },
    },
  })
}

Now when we run our development server: yarn workspace gatsby-theme-portfolio run develop and navigate to http://localhost:8000/___graphql we can see that our query:

{
  allProject {
    edges {
      node {
        name
        technologies
        url
        slug
      }
    }
  }
}

Returns us our slug field such that:

{
  "data": {
    "allProject": {
      "edges": [
        {
          "node": {
            "slug": "/project/my-gatsby-project"
          }
        },
        {
          "node": {
            "slug": "/project/my-second-project"
          }
        }
      ]
    }
  }
}

We can now use this field in our createPages lifecycle api (which we will define in a bit) to create custom pages for each project specifying the template and the path (slug). Each project will get it’s own page at the route/path of /project/project-name-slug. Let’s take a look at our createPages which allows us to create pages and pass in context object to define what the content should be for that page. We will do this in our gatsby-theme-portfolio/gatsby-node.js file. First, we are going to be making a graphql query to get all of our Project nodes that we have defined in our /src/data/Project.json file and then we will loop over all of the returned nodes and use the createPage action to define a project page. Let’s take a look at what that looks like:

const fs = require("fs")
// Make sure the src/data directory exists
exports.onPreBootstrap = ({ reporter }) => {
  const dataPath = "src/data"
  if (!fs.existsSync(dataPath)) {
    reporter.info(`creating the ${dataPath} directory`)
    fs.mkdirSync(dataPath)
  }
}

exports.sourceNodes = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type Project implements Node {
      slug: String! 
    }
  `
  createTypes(typeDefs)
}

exports.createResolvers = ({ createResolvers }) => {
  const resolveSlug = (source) => {
    const a = 'àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·/_,:;'
    const b = 'aaaaaaaaaacccddeeeeeeeegghiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz------'
    const p = new RegExp(a.split('').join('|'), 'g')

    const slugify = string => string.toString().toLowerCase()
      .replace(/\s+/g, '-')
      .replace(p, c => b.charAt(a.indexOf(c)))
      .replace(/&/g, '-and-')
      .replace(/[^\w\-]+/g, '')
      .replace(/\-\-+/g, '-')
      .replace(/^-+/, '')
      .replace(/-+$/, '')

    const slug = slugify(source.name)
    const basename = slugify(source.internal.type)

    return `/${basename}/${slug}`
  }

  createResolvers({
    Project: {
      slug: {
        resolve: source => resolveSlug(source),
      },
    },
  })
}

exports.createPages = async ({ actions, graphql, reporter }) => {
  const result = await graphql(`
    query {
      allProject {
        nodes {
          id
          slug
        }
      }
    }
  `)
  if (result.errors) {
    reporter.panic("error loading projects", result.errors)
    return
  }

  result.data.allProject.nodes.forEach( project => {
    actions.createPage({
      path: project.slug,
      component: require.resolve(`./src/templates/project.js`),
      context: {
        id: project.id  
      }
    }) 
  })  
}

To review this, we are getting all of our pages and looping over then using action.createPage and passing in an object to define the path, component and context. In our context object we are passing in the node ID so we can use a pageQuery with a variable to define the data object that will be passed as a prop into our React component inside /src/templates/project.js template. Let’s create the file in /src/templates/project.js and define our pageQuery and our React component definition:

import React from "react"
import { graphql } from "gatsby"

export const query = graphql`
  query($id: String!){
    project(id: {eq: $id}) {
      id
      slug
      name
      technologies
      url
    }
  }
`

const Project = ({ data: { project } }) => (
  <pre>
    { JSON.stringify(project, null, 4) }
  </pre>
)
export default Project

For pageQueries we can export a graphql tag query and Gatsby will recognize it as a pageQuery to be executed and the data injected into the Project component (defined below). For now, we will just stringify our project data to make sure that we’re getting the correct data for each project page that we navigate to. We can start up our development server by running: yarn workspace gatsby-theme-portfolio run develop and then navigating to http://localhost:8000/

We can see that we have two new pages created at http://localhost:8000/project/my-gatsby-project and http://localhost:8000/project/my-second-project and if we navigate to either of those pages we will see that it is picking up our template in /src/templates/project.js as well as passing in the correct data based on the project id:

{
    "id": "9b1a01a6-07d4-5be1-afc0-eac34c0193a0",
    "slug": "/project/my-second-project",
    "name": "My Second Project",
    "technologies": [
        "vue.js",
        "nuxt.js",
        "GraphQL",
        "Tailwind CSS"
    ],
    "url": "https://github.com/hashinteractive/gatsby-theme-reactor/tree/master/gatsby-theme-reactor"
}

Review What We’ve Done

This is fantastic! We’ve come a long ways. Let’s quick review. So far we’ve:

  • Setup our Yarn Workspaces so we can develop and test our Gatsby theme as a workspace dependency
  • Installed the gatsby-source-filesystem and gatsby-transformer-json plugins to source and transform our data which is stored at /src/data/Project.json
  • We defined a gatsby-node.js file which makes sure the folder /src/data folder exists and creates it if it does not
  • We added a slug field to our GraphQL Project type definition and used createResolvers api to resolve the slug field to be the slugified value of the Project’s name
  • Finally, we used the createPages api to get all our projects defined in the Project.json file and loop through them creating Gatsby pages for each project and passing the slug field as the path and the project id as the context
  • We defined a template in /src/templates/project.js and ran a pageQuery to get the data for each project and pass it to our React Component.

In Part Two of our tutorial we will look at how we can use our test-site workspace to extend our gatsby-theme-portfolio and also how we can add styles using Theme UI and finally how we can use component shadowing to override our theme’s components inside the test-site project to customize the look and feel of each component.