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
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 filesgatsby-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.
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!