Before you go, check out these stories!

0
Hackernoon logoCreating New Gatsby Theme with Typescript, MDX, and Theme-UI by@shetharp

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

Author profile picture

@shetharpArpit Sheth

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!

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!