Gatsby Themes provide a powerful way to share an opinionated set of configurations across multiple Gatsby sites. The features built into your Gatsby Theme will be abstracted out of your site and packaged as a dependency, providing an efficient way to develop similar Gatsby sites.
In this tutorial, we’ll set up a development workspace to build and demo a simple theme. Our workspace will be equipped with Typescript, ESLint, and Husky, so we can quickly start developing with a team. Our theme will support MDX pages, an MDX blog, frontmatter, syntax highlighting in code blocks, responsive images, and a custom theme built with the Theme-UI spec.
As you follow this guide, I encourage you to periodically reference my source code and commit history.
Although this guide is fairly thorough, it will help if you have some basic experience with Yarn workspaces, Gatsby, and Gatsby themes. If you don’t, I recommend skimming through these resources to get up to speed.
Most Gatsby themes leverage the concept of Shadowing in Gatsby and Theme-UI to make theme development more efficient. It may be worth reading up on these topics if you aren’t familiar.
gatsby-starter-theme-workspace
is a very simple and minimal startergatsby-theme-mdx
is a good starter for MDX blog support (I used this one)The
root
of your repoREADME
, package.json
, .gitiginore
package.json
should have workspaces for theme and demo folders.The
theme
that we will be creating1. Should have its own
README
, package.json
, .gitignore
, and gatbsy files2. The main
index.js
file can be left empty3. Rename the contents of the theme
package.json
to be your own@shetharp/gatsby-theme-candor
as the name of my theme. For the rest of the tutorial, you should replace @shetharp
with your npm username and gatsby-theme-candor
with your theme name.The
demo
site that will use your theme1. Should have its own
README
, package.json
, .gitignore
, and gatsby files2. Rename the contents of the demo
package.json
to be your ownpackage.json
. *
so that yarn knows to look for the package locally."dependencies": {
"gatsby": "^2.13.1",
"@shetharp/gatsby-theme-candor": "*",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
3. Update the demo
gatsby-config.js
file. Make sure under plugins, you include the name of your theme. It should match the name you set in the theme package.json
. For example:plugins: [{ resolve: '@shetharp/gatsby-theme-candor', options: {} }]
1. Install dependencies in your workspace
yarn
2. Run your demo site
yarn workspace demo develop
We’ll be adding a few dev dependencies and configurations to help enforce good code quality and catch errors, especially when working with a team. You can skip this section if the juice isn’t worth the squeeze.
Gatsby comes with native support for Typescript, but we need to add some configurations since we are working with Yarn workspaces.
1. Add Typescript as a dev dependency to your workspace
yarn add -W -D typescript
-W
flag tells yarn to add the dependency to your workspace's root package.json
2. Create a
tsconfig.json
file in the root of your repo{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"lib": ["dom", "es2017"],
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["./demo/src/", "./theme/src/"],
"exclude": ["node_modules", "demo/node_modules", "theme/node_modules"]
}
3. Add a
type-check
script in your root package.json
"scripts": {
"type-check": "tsc --noEmit"
}
type-check
it should run the typescript compiler.4. Add an example Typescript page in
demo/src/pages/example.tsx
import React from "react";
import { PageProps } from "gatsby";
export default function TypescriptExample(props: PageProps) {
return (
<>
<h1>Path:</h1>
Example page using typescript.
<pre>{props.path}</pre>
</>
);
}
5. Test that it is working. Run
yarn workspace demo develop
to make sure Gatsby is running, then navigate to http://localhost:8000/example to see the page.<pre>{props.asdf}</pre>
), you may notice that it updates your website, but Gatsby doesn't necessarily catch the error and crash. But if you run yarn type-check
, it should catch that error. In the rest of the set up, we'll automate type-checking into our workflow.We'll use ESLint to enforce consistent code syntax and formatting. (Learn more: Using ESLint and Prettier in a TypeScript Project).
1. Add ESLint along with its plugins for Typescript, React, and Prettier as dev dependencies to your workspace root
yarn add -W -D eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser
2. Add an
.eslintignore
file in your workspace rootnode_modules
**/node_modules/**
**/.cache/**
**/build/**
**/public/**
3. Add an
.eslintrc.js
file in your workspace rootmodule.exports = {
env: {
browser: true,
node: true,
},
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: "module", // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
},
},
settings: {
react: {
version: "detect", // Tells eslint-plugin-react to automatically detect the version of React to use
},
},
extends: [
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
"no-console": ["error", { allow: ["warn", "error"] }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-var-requires": "off",
"react/prop-types": ["warn", { skipUndeclared: true }],
},
};
4. Add a
.prettierrc.js
file in your workspace rootmodule.exports = {
printWidth: 120,
proseWrap: "preserve",
};
5. Add
lint
and lint:fix
scripts in your root package.json
"scripts": {
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"lint:fix": "yarn lint --fix",
"type-check": "tsc --noEmit"
},
6. Run
yarn lint
to view any lint errors. If you need to, restart your IDE to see lint errors in your code. Run yarn lint:fix
to auto fix most lint errors. You may need to fix remaining lint errors manually (or add eslint-disable
comments if you're feeling lazy).We'll use Husky to run the linter and type-checker before we commit and push code to git. These pre-commit and pre-push hooks will allow us to frequently catch syntax inconsistencies and type errors.
1. Add husky and lint-staged as dev dependencies
yarn add -W -D husky lint-staged
2. Add a
.huskyrc.js
file in your workspace rootmodule.exports = {
"hooks": {
"pre-commit": ["lint-staged"],
"pre-push": ["yarn type-check"]
}
}
3. Add a
.lintstagedrc
file in your workspace root{
"*.{js,jsx,ts,tsx}": ["yarn lint:fix"],
"{*.{json,md,mdx,yml,yaml}}": ["prettier --write"]
}
Commit and push these changes in your repo to git. You should see Husky in action!
Most Gatsby themes use Theme UI for developing themeable websites.
1. Add it as a dependency to your theme workspace, if it doesn't already include it.
yarn workspace @shetharp/gatsby-theme-candor add gatsby-plugin-theme-ui
2. Add it as a plugin to your theme's
gatsby-config.js
filemodule.exports = {
plugins: [
"gatsby-plugin-theme-ui",
]
}
3. To create your own theme, you'll need to leverage Shadowing in Gatsby to shadow the theme file from
gatsby-plugin-theme-ui
. Create a file in theme/src/gatsby-plugin-theme-ui/index.ts
. Create a theme object by following the Theme UI Spec and make it the default export of the file.4. You can build your theme from scratch if you're comfortable with Theme-UI, or you can base it off of a preset/existing theme. You can reference the
theme file source code on GitHub.gatsby-theme-candor
5. Tip: If you're making lots of changes to your theme, you might want to consider creating a theme preview page to see all of your theme's styles and components in one place. You can reference the gatsby-theme-candor Theme Preview page or its source code.
To support MDX in your theme, you will need to add the
gatsby-plugin-mdx
plugin if your theme doesn't have it yet. yarn workspace @shetharp/gatsby-theme-candor add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react
Make sure to add it to your theme's gatsby-config.js file. You can learn more about the plugin options in the plugin's official documentation.
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-mdx",
options: {
extensions: [".mdx", ".md"],
defaultLayouts: {
default: require.resolve("./src/templates/Page.tsx"),
},
},
},
]
}
1. In order for us to support MDX frontmatter queries, responsive images, and a content directory for posts, we'll need to add the
gatsby-source-filesystem
plugin to our theme.yarn workspace @shetharp/gatsby-theme-candor add gatsby-source-filesystem
2. Next, we will add the plugin to our theme's gatsby-config.js file and configure it to source posts and images. (Gatsby will automatically source from
src/pages
, so we don't need to include it).module.exports = {
plugins: [
{
resolve: "gatsby-source-filesystem",
options: {
name: "posts",
path: "src/posts",
},
},
{
resolve: "gatsby-source-filesystem",
options: {
name: "images",
path: "src/images",
},
},
]
}
3. Gatsby will throw an error if these paths don't exist in your set up. To avoid this, create folders for each of these paths (you can place empty files as placeholders within them) in your theme's workspace.
4. We also want to make sure consumers of our theme don't experience a Gatsby build error because one of our required paths doesn't exist for their set up. We can use the
onPreBootstrap
lifecycle hook to initialize these required directories for our users before Gatsby builds their site. Add this code to your theme's gatsby-node.js
file. This is a common Gatsby Theme Convention.const path = require("path");
const fs = require("fs");
const mkdirp = require("mkdirp");
exports.onPreBootstrap = ({ store, reporter }) => {
const { program } = store.getState()
const dirs = [
path.join(program.directory, "src/pages"),
path.join(program.directory, "src/posts"),
path.join(program.directory, "src/images"),
]
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
reporter.log(`creating the ${dir} directory`)
mkdirp.sync(dir)
}
})
}
We will use Gatsby Sharp to support responsive optimized images in our site.
1. Install the plugins in your theme workspace
yarn workspace @shetharp/gatsby-theme-candor add gatsby-plugin-sharp gatsby-transformer-sharp
2. Make sure to add these plugins to your theme's
gatsby-config.js
filemodule.exports = {
plugins: [
"gatsby-plugin-sharp",
"gatsby-transformer-sharp",
]
}
3. We also need to install the
gatsby-remark-images
plugin to support responsive images in our MDX content.yarn workspace @shetharp/gatsby-theme-candor add gatsby-remark-images
4. Make sure to add this to your theme's
gatsby-config.js
file under the plugin options for gatsby-plugin-mdx
module.exports = {
plugins: [
{
resolve: "gatsby-plugin-mdx",
options: {
extensions: [".mdx", ".md"],
defaultLayouts: {
default: require.resolve("./src/templates/Page.tsx"),
},
gatsbyRemarkPlugins: [
resolve: "gatsby-remark-images",
options: {
maxWidth: 800,
}
],
},
},
]
}
We want our theme to support frontmatter queries, so first add some frontmatter to your
.mdx
files. For example:---
title: Hello World! Catchy title from frontmatter!
author: Fina Mitai
date: 2020-07-30
featureImage: ./redwood.jpg
---
This is the markdown content. Lorem ipsum dolor sit **amet**.
We will reference the frontmatter with
pageContext
and query it with graphQL in the next steps.At this point, your website should be rendering MDX pages in your demo site's
src/pages
directory with your theme styles applied.Because we have blog posts (in the
src/posts
directory) being sourced outside of src/pages
, we need to give each post a slug for Gatsby to render the url into a page. (Alternatively, you could define the slug in the frontmatter, but in this example, we want Gatsby to generate slugs for us). Luckily, there's an official plugin we can use to avoid writing all this configuration manually.1. Install the
gatsby-plugin-page-creator
plugin to your theme's workspaceyarn workspace @shetharp/gatsby-theme-candor add gatsby-plugin-page-creator
2. Add
gatsby-plugin-page-creator
to your theme's gatsby-config.js
file. Give it the option to create pages from the src/posts
directory.module.exports = {
plugins: [
{
resolve: "gatsby-plugin-page-creator",
options: {
path: "src/posts",
},
},
]
}
3. Update the
gatsby-plugin-mdx
options in your theme's gatsby-config.js
file to create pages for posts using a Posts template. We'll create the Posts template in the next step.module.exports = {
plugins: [
{
resolve: "gatsby-plugin-mdx",
options: {
extensions: [".mdx", ".md"],
defaultLayouts: {
default: require.resolve("./src/templates/Page.tsx"),
posts: require.resolve("./src/templates/Post.tsx"),
},
gatsbyRemarkPlugins: [
resolve: "gatsby-remark-images",
options: {
maxWidth: 800,
}
],
},
},
]
}
4. Create a template for Posts in your theme's
src/templates/Post.tsx
fileimport React from "react";
import { graphql, useStaticQuery, PageProps } from "gatsby";
import Layout from "../components/Layout";
import { Badge, Text } from "theme-ui";
export type PostProps = PageProps & {
pageContext: {
frontmatter: { [k: string]: string };
};
};
const Post: React.FC<PostProps> = (props) => {
const { children } = props;
const data = useStaticQuery(graphql`
query {
site {
siteMetadata {
title
}
}
}
`);
return (
<Layout>
<Badge variant="accent">
<Text variant="mono">Post template</Text>
</Badge>
<Badge variant="highlight" marginLeft={1}>
{data.site.siteMetadata.title}
</Badge>
<h1>{props.pageContext.frontmatter.title}</h1>
<span>{props.pageContext.frontmatter.author}</span>
{children}
</Layout>
);
};
export default Post;
5. Run
yarn workspace demo develop
to see the .mdx
files in your demo site's src/posts
directory get turned into pages!To list out all the pages in your website, you'll want to create a blog index page. In this example, we'll name this page the Blog page. However, you can name this whatever you want, such as SiteIndex, SiteMap, etc.
1. Create a new file
src/pages/blog.tsx
in your demo workspace2. Add the following to the file
import React from "react";
import { PageProps, Link, graphql } from "gatsby";
import { Layout } from "@shetharp/gatsby-theme-candor";
import { Styled } from "theme-ui";
const BlogIndex: React.FC<BlogIndexProps> = (props) => {
const { data } = props;
const { nodes: pages } = data.allSitePage;
return (
<Layout>
<Styled.h1>Blog Index</Styled.h1>
<Styled.ul>
{pages.map(({ id, path, context: { frontmatter } }) => (
<Styled.li key={id}>
<Link to={path}>
<code>{path}</code>
</Link>
{frontmatter?.title && ` -- ${frontmatter.title}`}
</Styled.li>
))}
</Styled.ul>
</Layout>
);
};
export default BlogIndex;
export const pageQuery = graphql`
query AllPagesQuery {
allSitePage {
nodes {
id
path
context {
frontmatter {
author
date
excerpt
featureImage
title
}
}
}
}
}
`;
3. You can verify the graphql query works by trying it out in GraphiQL. Essentially, this query is getting all the pages in our site and providing their id, path, and frontmatter. In our BlogIndex component, we use the frontmatter to render the title of the page and the path to link to the page.
We are going to deploy our demo site using GitHub Pages to make it easy to view the demo site without having to pull down and build the repo.
For this example, we will be deploying the demo site to https://shetharp.github.io/gatsby-theme-candor/, where
will be your username and shetharp
will be the name of your repo on GitHub. gatsby-theme-candor
If you've ever deployed a Gatsby site using GitHub Pages, these steps should look familiar.
1. Install
gh-pages
as a dev dependency to your demo workspaceyarn workspace demo add -D gh-pages
2. Because the root url of our website will have its repo name in it, we need to define a prefix in the demo
gatsby-config.js
filemodule.exports = {
pathPrefix: "/gatsby-theme-candor",
}
3. Next, we add a script to our demo
package.json
to make it easy to deploy with one command{
"scripts": {
"deploy": "gatsby build --prefix-paths && gh-pages -d public"
}
}
4. Make sure your latest changes are committed to git. Then, change directory into your demo workspace
cd demo
and deploy!yarn deploy
5. Make sure you've configured your GitHub repo to source from the
gh-pages
branch for deployments. (You can follow these instructions to configure that).We're getting ready to publish our theme! Before we proceed, consider cleaning up or updating your
README
, package.json
, and gatsby-config
files to be well documented for your needs. Also, if you don't have an npm username, you should create an npm account now.1. At the beginning of this walkthrough, when setting up your yarn workspace, you should have given your theme workspace a name in its
package.json
file. Verify that the name you provided is namespaced, typically using your npm username (e.g. "name": "@shetharp/gatsby-theme-candor"
). 2. Verify that you are logged into npm by running
npm whoami
npm login
to enter your username, password, and email.3. Change directories into your theme workspace
cd theme
4. Publish to npm!
npm publish --access-public
5. If you decide to publish new updates to your theme later on, you will need to update the version number in your theme
package.json
. Verify that the changes you've made to your theme are reflected in your updated version number. It is common practice to use semantic versioning to indicate breaking changes or patches.Congrats on setting up your Gatsby Theme! In this tutorial we made an effort to set up a robust repository for theme development with a team--so share it with others and start collaborating!
As you continue configuring your theme to meet your needs, I encourage you to view the
demo site, source code, and commit history.gatsby-theme-candor
If you found this tutorial to be a useful starting point, feel free to build your next gatsby site or theme on top of
gatsby-theme-candor
. Let me know what you build @shetharp!