Creating New Gatsby Theme with Typescript, MDX, and Theme-UI

Written by shetharp | Published 2020/07/11
Tech Story Tags: gatsby | gatsbyjs | theme-ui | mdx | typescript | javascript | react | coding

TLDR Themes provide a powerful way to share an opinionated set of configurations across multiple Gatsby sites. The features built into your theme will be abstracted out of your site and packaged as a dependency. In this tutorial, we’ll set up a development workspace to build and demo a simple theme. 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.via the TL;DR App

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.
Skip to the end
As you follow this guide, I encourage you to periodically reference my source code and commit history.

Prerequisites

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.

1. Set up workspace

1.1 Clone a starter workspace repo

1.2 Make the repo your own

The
root
of your repo
  1. Should have the following files:
    README
    ,
    package.json
    .gitiginore
  2. Should have the following folders: theme and demo. Feel free to rename these folders.
  3. The root
    package.json
    should have workspaces for theme and demo folders.
The
theme
that we will be creating
1. Should have its own
README
,
package.json
.gitignore
, and gatbsy files
2. The main
index.js
file can be left empty
3. Rename the contents of the theme
package.json
to be your own
  • You can name your theme whatever you want, but you should make sure it is scoped using your npm username if you plan on publishing it. (You should create an account now if you don’t have one).
  • I’ll be using
    @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 theme
1. Should have its own
README
,
package.json
.gitignore
, and gatsby files
2. Rename the contents of the demo
package.json
to be your own
  • Make sure under the dependencies section, you have a dependency for your theme. The name of this dependency should match the name you set in the theme
    package.json
    .
  • Because we have not published the theme yet as an npm package and will be developing it locally, set the version to
    *
    so that yarn knows to look for the package locally.
  • The dependencies section should look something like this:
"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.3 Make sure your setup is working

1. Install dependencies in your workspace
yarn
2. Run your demo site
yarn workspace demo develop
  • This command tells yarn to run the develop script from the package.json in your demo workspace folder.

2. Set up Typescript, ESLint, and Husky

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.

2.1 Set up Typescript

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
  • The
    -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"
}
  • If you run yarn
    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.
  • If you modify the file to cause a typescript error (e.g.
    <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.

2.2 Set up ESLint

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 root
node_modules
**/node_modules/**
**/.cache/**
**/build/**
**/public/**
3. Add an
.eslintrc.js
file in your workspace root
module.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 root
module.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).

2.3 Set up Husky

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 root
module.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!

3. Style your theme

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
file
module.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
gatsby-theme-candor
theme file source code on GitHub.
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.

4. Set up MDX for pages and posts

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"),
        },
      },
    },
  ]
}

4.1 Set up Gatsby Source Filesystem

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)
        }
    })
}

4.2 Set up Gatsby Sharp for images

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
file
module.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,
          }
        ],
      },
    },
  ]
}

4.3 Set up MDX Frontmatter

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.

5. Programmatically Creating Pages

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 workspace
yarn 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
file
import 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!

6. Set up a Blog Index Page

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 workspace
2. 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.

7. Deploy your demo to GitHub Pages

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
shetharp
will be your username and
gatsby-theme-candor
will be the name of your repo on GitHub.
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 workspace
yarn 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
file
module.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).

8. Publish your theme to npm

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"
).
  • This will help the Gatsby and npm community keep track of who published the theme and avoid name collisions.
  • You will have to use your own namespace for this step.
2. Verify that you are logged into npm by running
npm whoami
  • If you're not logged in, run
    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
  • You should be able to see your newly published package in your npm profile.
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.

Conclusion

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
gatsby-theme-candor
demo site, source code, and commit history.
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!

Published by HackerNoon on 2020/07/11