Set up a clean Gatsby blog with a minimal UI and without any starters

05-02-2020

Some introduction of Gatsby...

As you may already know, Gatsby is a wonderful static website generator for React. It renders all files by client pages - HTML, CSS, and JS during the transpile time instead of runtime, and provides you flexibility choosing your server hosting methods. Not only providing a great performance on page-loading, but also improving the SEO, Gatsby is ideal for building market front-page or building a highly extensible blog site. For example, this site is built using Gatsby. For a developer like me with years of experience on React, it is really helpful to let me focus on the blog content to write rather than spending a lot of time on setting up frameworks. Gatsby also provides useful configurations to let you create pages from different data sources, using GraphQL as a medium. I'm using GraphQL at work for building communications between front-end and back-end in an efficient and flexible way (it may be another topic for another blog then), but the usage here in Gatsby really gave me inspiration. It enables the usage of a CMS and complete separation of the codes and the content of a website.

And some inspiration of the starting point of mine

Gatsby, as a "framework on a framework", provides some "too" simple ways to build a deployable site quickly. I mean the starters, which is an interesting business model to let developers in the community help each other by learning everyone's template, or to give those potential users who know little coding a way to get start with this great framework. After installing gatsby-cli, you can just use gatsby new <name of your repo> <path of the starter repo> to set up a new site with everything there. However, sometimes you (like me) want to get more control on the starting step to avoid:

  1. an overwhelmed list of dependencies;
  2. issues of compatibility of dependencies and maybe with your gatsby-cli, or you just want to update some of the functions but it couldn't work after that;
  3. detective work for code improvement or customization

The docs of Gatsby guide you a quick start with gatsby new. But how should it be if you want to start clean just from an npm init? I just want to log the work I've done for this portfolio page of mine, and maybe those logs can also help you a little :)

Setting up Gatsby and Typescript

Init

Some background: I got sweeties from using Typescript in large projects at work, and want to keep it as a convention also in every private coding, (OK, no matter if it is necessary), just to take every chance to get used to it.

Getting typescript and tsc installed globally, I ran

tsc --init

to initialize the code repo of the project. In comparison to a simple npm init, it set a default tsconfig.json and tslint.json. For any of the syntax warnings, I always take a look directly in the tsconfig.json to see what is missing. In this way, I don't need to go to the Typescript docs every time when I need help.

Dependencies

My first installed dependencies:

"dependencies": {
    "gatsby": "^2.19.32",
    "gatsby-plugin-tslint": "0.0.2",
    "gatsby-plugin-typescript": "^2.2.2",
    "react": "^16.13.0",
    "react-dom": "^16.13.0"
  },
"devDependencies": {
    "@types/react": "^16.9.23",
    "@types/react-dom": "^16.9.5",
    "tslint": "^6.0.0",
    "tslint-config-prettier": "^1.18.0",
    "tslint-loader": "^3.5.4",
    "tslint-plugin-prettier": "^2.1.0",
    "tslint-react": "^4.2.0",
    "typescript": "^3.8.3"
}

Linting

I'm also using VS Code as IDE, with prettier installed and I've set prettier in the settings of VS Code and let it use the rules in tslint.json:

{
    "defaultSeverity": "error",
    "extends": [
        "tslint:latest",
        "tslint-react",
        "tslint-config-prettier"
    ],
    "rules": {
        "prettier": true,
        "jsx-no-multiline-js": false,
        "jsx-no-lambda": false,
        "interface-name": false
    },
    "rulesDirectory": ["tslint-plugin-prettier"]
}

Gatsby Config

There are several files generated by a default gatsby new command. The one we need for the first step is gatsby-config.js:

module.exports = {
    siteMetadata: {
        ...
    },
    plugins: [
        `gatsby-plugin-typescript`,
    ]
}

This file contains the general configuration for the gatsby site. The metadata can be retrieved by a GraphQL query in Gatsby conveniently. The plugins list will be extended in the future. But for the first step, I just include gatsby-plugin-typescript to let Gatsby know that it should build ts and tsx. This plugin has options to allow the existence of mixed file formats like js and jsx. The official documentation with more details is here.

Support for a simple content structure

Now we have a runnable code repo. Gatsby searches for src/pages/index.tsx for building the entry page. Actually, Gatsby builds a page for each component inside the src/pages directory. I've added

import React from "react"
export default () => <h1>Hello Gatsby!</h1>

in this index.tsx file to quickly check if everything works well. Run gatsby develop, or add an npm script in package.json (if you don't have a gatsby-cli in global environment, this should be a must-to-have) for making this check. Open the browser and go to http://localhost:8000, if you have seen this:

first test

first test

then everything is going correctly.

Types for GraphQL

You may want to check another super exciting thing here: http://localhost:8000/___graphql:

first look at graphql

first look at graphql

This is the GraphiQL playground that Gatsby prepared for us. Later we can write our GraphQL queries based on the explorer here. For example, we can get the metadata we just defined in site.siteMetadata. I always use the GraphiQL as a dictionary and look up for necessary contents I would like to hold onto the front-end.

Now there is a question: is there a way to get the GraphQL schema into a Typescript declaration file so that we can use it in our codes for components? For that, we need graphql-codegen. Normally, we can install @graphql-codegen/cli, and run an npm script to generate our graphql-types.d.ts file, by adding a yaml:

overwrite: true
schema: "http://localhost:8000/___graphql"
generates: 
  ./src/graphql-types.d.ts:
    plugins:
      - "typescript"

Fortunately, there is a Gatsby plugin gatsby-plugin-graphql-codegen. We can install it and add it into gatsby-config.js to let the type generation run automatically. From now on, we can

import { SiteSiteMetadata } from './graphql-types.d.ts'

for our siteMetadata, if we want to use its property in a component.

Plugins for images, markdown, and codes

The idea is to have a content folder in somewhere else outside of the src, which contains our blog posts written in markdown and images. I keep this directory in the repo for the convenience of site building processes, but I planned to keep it in a sub-module that is not synced with the source code repo, but with cloud storage as a content management system. I'm now using the gatsby-source-filesystem plugin to achieve this. There are several plugins for AWS S3 - you may take a look at these if that matches your tastes. There are several useful plugins helping you build static pages from different formats - I'm used to writings with markdown, so this set is an example for this purpose:

  1. gatsby-transformer-remark: parses markdown files into html - it will add allMarkdownRemark and markdownRemark in the GraphQL service. You can get the blog texts, file paths, or front-matters in markdown yaml for holding information like authors and dates, by querying them.
  2. gatsby-remark-images: improves the parsing process of images in markdown. You can define the default styles for an image container in the options in gatsby-config.js. This plugin needs gatsby-plugin-sharp for using the "blur up" technique to load large images efficiently. Here are more details about sharp.
  3. gatsby-remark-prismjs for displaying code blocks with syntax highlights and style templates.

"styled" components

I'm using styled-components to build layout and styling. I've created a GlobalStyle component with it, and placed it into the src/styles directory:

import { createGlobalStyle } from 'styled-components'

/**
 * The font size is no more controlled acc.to vw to improve readability
 * esp. if a phone is being rotated over (fonts in portrait view will not be smaller than in landscape view)
 * px2vw function is only used for calculating margins and component heights
 */
export const GlobalStyle = createGlobalStyle`
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    :root {
        --primary-color: #82318e;
        --secondary-color: #fa7268;
        --bg-color: #ffffff;
        --grey: #a9a9a9;
        font-size: ${(16 * 768) / 1024}px;
        font-family: 'Open Sans', 'Noto Sans SC', sans-serif;
        
        /* tablet */
        @media (min-width: 768px) {
            font-size: ${(16 * 920) / 1024}px;
        }
        /* desktop */
        @media (min-width: 1024px) {
            font-size: 16px;
        }

        .gatsby-resp-image-figcaption {
            text-align: center;
            padding: 0.4rem 0;
            font-size: 0.7rem;
            font-style: italic;
        }
    }

    body {
        background-color: var(--bg-color);
        line-height: 1.8;
    }
`;

To inject global styles, I've put it into the API files gatsby-browser.js and gatsby-ssr.js to support both browser or server-side rendered use scenarios. The codes are as following:

import React from "react";
import { Helmet } from "react-helmet";
import { GlobalStyle } from "./src/styles";

require("prismjs/themes/prism-tomorrow.css");

export const wrapRootElement = ({ element }) => {
  return (
		<>
			<Helmet>
				<link
					href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&family=Open+Sans:wght@400;700&family=Ubuntu:wght@400;700&display=swap"
					rel="stylesheet"
				></link>
			</Helmet>
			<GlobalStyle />
			{element}
		</>
  );
};

I've prepared other templates for blog posts and other portfolio pages separately.

Building pages

The last step I want to log is how to build blog post pages from the markdown files in the content directory. As a static site generator, Gatsby generates those pages during the building process. You need to compute this process in the API file gatsby-node.js. To be concretized, there are several steps in this file:

  1. You create additional GraphQL nodes using the onCreateNode for each Remark record generated by gatsby-transformer-remark from the markdown files.
  2. You query the Remark records in the createPages API: In allMarkdownRemark fields, an edge is a post, and the nodes of the edge are properties, including the ones you added in onCreateNode.
  3. You invoke createPage for each edge.
  4. You may run other APIs like onPostBuild for any of other side effects.

Here is my example:

const { createFilePath } = require(`gatsby-source-filesystem`)
const path = require(`path`)

exports.onCreateNode = ({ node, getNode, actions }) => {
    const { createNodeField } = actions
    if (node.internal.type === `MarkdownRemark`) {
        const relativePath = createFilePath({ node, getNode })
        const [ category, slug, ...rest ] = relativePath.split('/').filter(el => !!el)
        createNodeField({
            node,
            name: `slug`,
            value: slug,
        })
    }
}

exports.createPages = async ({ graphql, actions, reporter }) => {
    const { createPage } = actions
    const result = await graphql(`
        {
            allMarkdownRemark(
                filter: { fields: { category: { eq: "blogs" }}}
                sort: { fields: [frontmatter___date], order: DESC }
            ) {
                edges {
                    node {
                        fields {
                            slug
                        }
                        frontmatter {
                            title
                            date(formatString: "MM-DD-YYYY")
                        }
                        html
                        wordCount {
                            words
                        }
                    }
                }
            }
        }
    `);

    if (!!result.errors) {
        reporter.panicOnBuild(`Error while running GraphQL query by creating pages.`)
        return
    }

    const posts = result.data.allMarkdownRemark.edges
    posts.forEach((edge, index) => {
        const previous = index === 0 ? null : posts[index-1].node
        const next = index === posts.length-1 ? null : posts[index+1].node
        createPage({
            path: edge.node.fields.slug,
            component: path.resolve(`./src/templates/blog-post.tsx`),
            context: {
                previous,
                next,
                post: edge.node
            }
        })
    })
}

It is pretty much! I will continue logging my private work and my learning process on this site. Besides the first long post, I may focus on detailed issues in the next ones, like how to use the Gatsby router and the issues I've been solving on formatting captions in gatsby-remark-image. I'm also learning Vue, and trying to write an article about the comparison between Vue, React, and Angular from my "learning-by-doing" experience.

Hopefully this will help you a little to start with building your own site with Gatsby :)

© 2021 Shan