GatsbyJS is a fast static site generator with a big ecosystem of themes. But a Gatsby theme isn't what it sounds like. It's not a way to style and organize your website. Instead, it's a versioned plugin package that can be updated like any other package.

Gatsby themes are “plugins that include a gatsby-config.js file and add pre-configured functionality, data sourcing, and/or UI code to Gatsby sites. You can think of Gatsby themes as separate Gatsby sites that can be put together and allow you to split up a larger Gatsby project!

This blog post will explain how you can create a basic Gatsby theme for a blog, how to configure your development environment, and how you can use your local theme with existing Gatsby projects.

Prerequisites

Yarn Workspaces

Yarn Workspaces allows you to split your project into sub-components. We need this to create a theme and a site to test our theme, both in the same directory. So we'll create two workspaces, one called gatsby-theme-posts and the other test-site. For now, create a folder called blog-theme and add a package.json file with the following:

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

The name of the workspaces should be the same as the name property in the package.json file. Next, inside the blog-theme folder, create two new folders called gatsby-theme-posts and test-site.

Inside your blog-theme directory, you should now have one file and two directories:

  • [f] package.json
  • [d] gatsby-theme-posts
  • [d] test-site

Configuring gatsby-theme-posts

Inside the gatsby-theme-posts folder, create a package.json file and add this:

{
  "name": "gatsby-theme-posts",
  "version": "1.0.0",
  "main": "index.js",
  "script": {
    "build": "gatsby build",
    "clean": "gastby clean",
    "develop": "gatsby develop"
  }
}

The name property corresponds to the name of the Yarn workspace we set up earlier in the root-level folder, while the main property is necessary for Node to properly resolve the gatsby-theme-posts package (remember that a Gatsby theme is basically a package).

Peer Dependencies

gatsby-theme-posts needs peer dependencies to ensure that the end user of your theme can choose any compatible version of Gatsby. Run yarn workspace gatsby-them-posts app -P gatsby react react-dom.

Development Dependencies

Any new Gatsby theme starts out as a regular Gatsby site. So let's add the required dependencies: yarn workspace gatsby-theme-posts add -D gatsby react react-dom. If you open the package.json file inside gatsby-theme-posts, you'll see something like this:

{
  "name": "gatsby-theme-posts",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
  "build": "gatsby build",
  "clean": "gastby clean",
  "develop": "gatsby develop"
},
"peerDependencies": {
  "gatsby": "^2.32.4",
  "react": "^17.0.1",
  "react-dom": "^17.0.1"
},
"devDependencies": {
  "gatsby": "^3.0.0-next.7",
  "react": "^17.0.1",
  "react-dom": "^17.0.1"
 }
}

Configuring test-site

Now it's time to set the basic foundation for our test site. We're going to install the minimum number of packages we need to run a Gatsby site. We're also going to add the gatsby-theme-post dependency. First, create a package.json file inside test-site and add this:

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

Remember that the name property has to be the same as the one we set up in the Yarn Workspaces section.

Next, let's install the dependencies. Run yarn workspace test-site add gatsby react react-dom gatsby-theme-posts@*. Think of running yarn-workspace test-site-add as running yarn add inside the test-site folder. We add gatsby-theme-posts@* without a version number because we're referencing an unpublished theme.

Note: if you're using ZSH, the package needs to be in quotes, e.g. "gatsby-theme-posts@_".

If you open the package.json file, you should see the gatsby-theme-posts package as a dependency:

dependencies": {
  "gatsby": "^2.32.3",
  "gatsby-theme-posts": "*",
  "react": "^17.0.1",
  "react-dom": "^17.0.1"
},

Now let's see if our theme is a dependency in our test-site workspace. Run yarn workspace info in the root folder blog-theme. You should see something like this:

A Test Run

Let's verify that both test-site and gatsby-theme-posts are working. You should see a site serving a default Gatsby 404 page. Inside each folder, run the following commands:

  • yarn workspace test-site run develop
  • yarn workspace gatsby-theme-posts run develop
A Gatsby 404 page
We don't have any content yet, so this is normal

Adding Data to gatsby-theme-posts

Now that we have both Gatsby sites working (remember that a Gatsby theme starts as a Gatsby site), let's add some data to gatsby-theme-posts. Our theme will pull posts from a YAML file, but we'll need a couple of dependencies for that first. Run yarn workspace gatsby-theme-posts add gatsby-source-filesystem gatsby-transformer-yaml.

  • gatsby-source-filesystem will let you load \*.yml files
  • gatsby-transformer-yml helps parse YAML data

Now that we have our dependencies set up, let's configure Gatsby to use its plugin. Create a gatsby-config.js file inside the gatsby-theme-posts directory and add this:

module.exports={
  plugins:[
    {
        resolve: "gatsby-source-filesystem",
        options:{
            path: "data"
        },
    },
    {
        resolve: "gatsby-transformer-yaml",
        options:{
            typeName:"Post",
        },
    },
  ],
}

Next, create a directory inside gatsby-theme-posts called data. Inside of data, create a new file called posts.yml and add this:

- title: Setup Neovim for Flutter
  publish_date: 2021-02-10
  summary: Flutter is simply dummy text of the printing and typesetting 	industry. Lorem Ipsum has been the industry's standard dummy text ever    since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book
  category: neovim

- title: Setup Neovim for Javascript
  publish_date: 2021-01-20
  summary: Javascript is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book
  category: neovim

- title: Golang and Rabbitmq
  publish_date: 2021-01-05
  summary: Rabbitmq is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book
  category: golang

- title: Golang and Neo4j
  publish_date: 2021-01-05
  summary: Neo4j is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book
  category: golang

Now run gatsby develop inside gatsby-theme-posts. Open the GraphiQL explorer and make a test query on allPost.

query MyQuery {
  allPost {
    edges {
      node {
        tile
        publish_date
        summary
      }
    }
  }
}

You should see a list with the posts, their title, publish date, and summary.

So far so good! Now that we can query data for Gatsby, let's create the pages to display the list of posts, as well as the individual posts. But first, let's validate that the data folder exists.

Running gatsby-theme-posts without an existing data folder will return an error because gatsby-source-filesystem can't find it. So let's fix that. Create a gatsby-node-js file inside of gatsby-theme-posts. Add this:

const fs = require("fs")

exports.onPreBootstrap = ({ reporter }) => {
  const contentPath = "data"
  if (!fs.existsSync(contentPath)) {
    reporter.info(`creating the ${contentPath} directory`)
    fs.mkdirSync(contentPath)
  }
}

With the onPreBootstrap API hook, we can validate that the folder exists before loading any plugins. Later on, we're going to make this hook accept configuration parameters. This way, we can use different names for the folder that will hold the posts data.

Our next step is to create data-driven pages. For this, we'll need to do three things:

  • Define the Post type
  • Define resolvers for custom fields that we might need on the Post type
  • Query for posts

Creating the Post Type

To create a new Type we need to customize our GraphQL schema. For that, we need to implement createTypes inside the gatsby-node.js file. Just under the onPreBootstrap hook, add the following:

exports.sourceNodes = ({ actions }) => {
  actions.createTypes(`
    type Post implements Node @dontInfer {
      id: ID!
      title: String!
      publish_date: Date!
	  summary: String!
      slug: String!
    }
  `)
}

We've created the Post type and have added the id and slug fields to it. Since we've explicitly added those fields, we need to implement @dontInfer.

Creating the Resolver for the Slug

Since our data doesn't have a slug field, we need to generate it for each post. So we'll take the title and generate a slug from it with the following:

exports.createResolvers = ({ createResolvers }) => {
  const basePath = "/"
  // Quick-and-dirty way to convert the title into URL-friendly slugs.
  const slugify = str => {
    const slug = str
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/(^-|-$)+/g, "")
    return `/${basePath}/${slug}`.replace(/\/\/+/g, "/")
  }
  createResolvers({
    Post: {
      slug: {
        resolve: source => slugify(source.title),
      },
    },
  })
}

Now run yarn workspace gatsby-theme-posts develop and head over to http://localhost:8000/___graphql. Over there, add the slug field to the query:

query MyQuery {
  allPost {
    edges {
      node {
        tile
        publish_date
        summary
+       slug
      }
    }
  }
}

You can now see that the slug field is being returned from our query.

A Query For Posts

As a final step, we need to create data-driven pages from our GraphQL aueries. For this, we're going to use the createPages API hook. This hook runs in Gatsby's bootstrap sequence. Since we're exporting the hook, pages will be created as one of the steps in the sequence.

Let's add the createPages hook just below the createResolvers hook inside the gatsby-node.js file.

exports.createPages = async ({ actions, graphql, reporter }) => {
  const basePath = "/"
  actions.createPage({
    path: basePath,
    component: require.resolve("./src/templates/posts.js"),
  })
}

Right now we're creating the root page for listing the posts, so let's create the posts.js component. Inside gatsby-theme-posts, create an src/templates folder and add a file with the following:

import React from "react";
import { graphql, useStaticQuery, Link } from "gatsby";

const PostsTemplate = () => {
  const data = useStaticQuery(graphql`
    query {
      allPost(sort: { fields: publish_date, order: ASC }) {
        nodes {
          id
          title
          publish_date
          summary
        }
      }
    }
  `);
  const postsListing = data.allPost.nodes;
  return (
    <>
      <ul>
        {postsListing.map((post) => (
          <li key={post.id}>
            <strong>
              <Link to={post.slug}>{post.title}</Link>
            </strong>
            <br />
            {new Date(post.publish_date).toLocaleDateString("en-US", {
              month: "long",
              day: "numeric",
              year: "numeric",
            })}
          </li>
        ))}
      </ul>
    </>
  );
};
export default PostsTemplate;

We'll be using templates for both the post listing page and the post page. Let's test the root page. Run yarn workspace gatsby-theme-posts develop and head over to http://localhost:8000/. You should see something like this:

If you click on any of the posts, you'll get a Gatsby 404 page. That's because we haven't created a page for each post. So let's do this now. We need to query the posts and use the createPage hook in a loop. Inside the createPages hook, just below actions.createPage, add the following:

const result = await graphql(`
    query {
      allPost(sort: { fields: publish_date, order: ASC }) {
        nodes {
          id
          slug
        }
      }
    }
  `)
  if (result.errors) {
    reporter.panic("error loading posts", result.errors)
    return
  }
  const posts = result.data.allPost.nodes
  posts.forEach(post => {
    const slug = post.slug
    actions.createPage({
      path: slug,
      component: require.resolve("./src/templates/post-details.js"),
      context: {
        postID: post.id,
      },
    })
  })

Let's also create the post-details.js template inside gatsby-theme-posts/src/templates.

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

export const query = graphql`
  query($postID: String!) {
    post(id: { eq: $postID }) {
      title
      publish_date
      summary
    }
  }
`;

const PostDetailsTemplate = ({ data: { post } }) => (
  <>
    <div>
      <h2>{post.title}</h2>
      <p>
        {new Date(post.publish_date).toLocaleDateString("en-US", {
          month: "long",
          day: "numeric",
          year: "numeric",
        })}
      </p>
      <p>{post.summary}</p>
    </div>
  </>
);
export default PostDetailsTemplate;

Save the file and run yarn workspace gatsby-theme-posts develop. Now you should be able to view the post details page.

A Dynamic Theme

We're almost there! Now we'll make the theme a little more useful. Let's allow it to take options by updating the gatsby-config.js and gatsby-node.js files. Update gatsby-theme-posts/gatsby-config.js with the following:

module.exports = ({ contentPath = "data", basePath = "/" }) => ({
  plugins: [
    {
      resolve: "gatsby-source-filesystem",
      options: {
        path: contentPath,
      },
    },
    {
      resolve: "gatsby-transformer-yaml",
      options: {
        typeName: "Post",
      },
    },
  ],
});

This snippet of code sets contentPath and basePath. These two new parameters will be provided to gatsby-node.js as a second argument to the API hooks. Now update gatsby-theme-posts/gatsby-node.js:

exports.onPreBootstrap = ({ reporter }, options) => {
  const contentPath = options.contentPath || "data";
  ....
};


exports.createResolvers = ({ createResolvers }, options) => {
  const basePath = options.basePath || "/";
  ...
};

exports.createPages = async ({ actions, graphql, reporter }, options) => {
  const basePath = options.contentPath || "/";
  ...
};

Since we've now modified the theme to use a function export on gatsby-config.js, we can no longer run the theme as we did before. Let's switch to test-site.

Using the Theme

Create a gatsby-config.js file in the test-site directory:

module.exports = {
  plugins: [
    {
      resolve: "gatsby-theme-posts",
      options: {
        contentPath: "posts",
        basePath: "/posts",
      },
    },
  ],
};

contentPath is the directory where the site will be looking for data (instead of the default data folder). basePath is the root for the list of all posts.

Run yarn workspace test-site develop and head over to http://localhost:8000/. You should see Gatsby's 404 page, since we don't have a root page, but you can also see /posts.

If you click on /posts you won't see anything, so let's fix that. Inside test-site, you'll need a new directory called posts. This was the name we used for contentPath. Copy the posts.yml file in gatsby-theme-posts/data and paste it in the new directory. Then run yarn workspace test-site develop and head over to http://localhost:8000/posts.

A list of posts!

Using Your Theme on an Existing Gatsby Site

Congratulations! You've just created your first theme. If you now want to use your new theme locally on an existing Gatsby project, follow these steps:

  • Create a folder and place your Gatsby site inside.
  • Create a Yarn Workspace like we did in the Yarn Workspaces section
  • Copy the folder of your theme (gatsby-theme-posts) into the new workspace.
  • Install the theme as a package: yarn workspace <your-site> add gatsby-theme-posts@*
  • Update gatsby-config.js and add the new plugin, like we did here.

Remember, a Gatsby theme starts as a Gatsby site. To make it a theme, you need to convert it so it uses a function export on gatsby-config.js. Then, you can install it as a dependency on existing projects, just like any other npm package. Happy coding!