Chapter 13. Advanced Topics in Gatsby

In the previous chapters, we’ve examined some of the most important beginning and intermediate use cases when developing Gatsby sites. But there are also many features available in Gatsby for more advanced use cases. For example, as a Gatsby developer, you may wish to automate certain tasks during Gatsby site setup, customize the GraphQL schema emitted by Gatsby’s schema generation process, configure how Gatsby bundles sites, add components to Markdown files managed in Gatsby, or optimize your site’s performance beyond Gatsby’s defaults.

In this wide-ranging chapter, we’ll cover some advanced use cases and features that go well beyond Gatsby’s default capabilities that are available from starters. We’ll explore creating Gatsby recipes, an example of infrastructure as code (IaC); adding components to Markdown using the MDX library; customizing Gatsby’s GraphQL schema and bundling process; and optimizing performance using Gatsby’s cache layer and other techniques relevant to both development and production use cases.

Creating Recipes

In this section we’ll explore recipes in Gatsby, which are configuration files or scripts that Gatsby ingests in order to generate predefined and consistent environments across development and production with minimal overhead. We’ll also discuss the concept of infrastructure as code and how to automate common Gatsby site operations.

Infrastructure as Code

IaC involves the notion of managing discrete environments (such as development, testing, and production) through configuration files and scripts that ensure consistency across your site builds. Differences between environments, such as differing Node.js versions, are a common but often subtle source of problems for developers. IaC is intended to minimize this sort of “drift.”

In Gatsby, configuration files describe the resources required by your project, including the dependencies and specific versions relied on by your Gatsby site. In addition, many Gatsby developers leverage plugins and themes that also need to be handled with cross-environment consistency. Because configuration files can be managed through source control, like other source files in Gatsby projects, we can use them to instantiate consistent environments every time we need to. This reduces the risk of errors caused as environments drift and provides a shared foundation for automated tasks.

Automating Site Operations with Recipes

Because Gatsby recipes are a relatively new feature, introduced late in the Gatsby 2.0 release cycle, you may need to upgrade your version of Gatsby to the latest version in order to access recipe features:

$ npm install -g gatsby-cli@latest
$ npm install gatsby@latest

Then, run the following command in the root of an existing Gatsby project to access the list of available recipes:

$ gatsby recipes

You’ll see output in your terminal that looks like the following and allows you to select from a variety of available recipes:

Select a recipe to run
>>Add a custom ESLint config
  Add Jest
  Add Gatsby Image
  Add Gatsby Theme Blog
  Add Gatsby Theme Blog Core
  Add Gatsby Theme Notes
  Add persistent layout component with gatsby-plugin-layout
  Add Theme UI
  Add Chakra UI
  Add Emotion
  Add support for MDX Pages
  Add support for MDX Pages with images
  Add Styled Components
  Add Tailwind
  Add Sass
  Add TypeScript
  Add Cypress testing
  Add animated page transition support

When you select one of these recipes and hit the Return key, your Gatsby site will automatically be equipped with the features it provides, set up on your behalf by Gatsby. Recipes are written using Markdown and MDX, which we explore further in the next section. This means that recipes are not only easy to write, but also familiar to Gatsby developers across your team who have experience with MDX. As an example, here is the recipe to set up Styled Components, which is one of the options in the preceding list:

# Setup Styled Components

[Styled Components](https://styled-components.com/) is visual primitives for the component age.
Use the best bits of ES6 and CSS to style your apps without stress

---

Install necessary NPM packages

<NPMPackage name="gatsby-plugin-styled-components" />
<NPMPackage name="styled-components" />
<NPMPackage name="babel-plugin-styled-components" />

---

Install the Styled Components plugin in gatsby-config.js

<GatsbyPlugin name="gatsby-plugin-styled-components" />

---

Sweet, now it's ready to go.

Let's also write out an example page you can use to play
with Styled Components.

<File
  path="src/pages/styled-components-example.js"
  content="https://gist.githubusercontent.com/KyleAMathews/ >>>  
34541b87c4194ba2290eedbe8a0b1fe0/raw/ >>>
dba4d3ffecb5f2a3a36e0e017387185a9835c685/styled-components-example.js"
/>

---

Read more about Styled Components on the official docs site:

https://styled-components.com/

If there isn’t a recipe in this list that suits your needs, you can author your own or use one contributed to the ecosystem by the Gatsby community. To run a local Gatsby recipe located on your machine, execute the following command:

$ gatsby recipes ./my-local-recipe.mdx

To run a remote recipe, such as one contributed by the community, you’d execute a command like the following:

$ gatsby recipes https://my-recipes-host.com/remote-recipe.mdx

Now that we’ve introduced the concept of recipes, let’s dig into those strange JSX elements you may have noticed in the Styled Components example, like <NPMPackage> and <File>. In the next section, we’ll explore MDX, a means of adding JSX components directly into your Markdown files (and a technology that can be leveraged for much more than Gatsby recipes!).

Note

For more information about Gatsby recipes, consult the Gatsby documentation’s guide to infrastructure as code as well as the Gatsby recipes announcement and documentation on developing recipes.

Adding Components to Markdown with MDX

In the previous section, we saw that Gatsby recipes are written with JSX elements interpolated into Markdown files. But what are these elements, and how is it possible to write JSX elements directly within Markdown? The answer lies in MDX.

MDX is an extension to the Markdown format that allows Markdown and JSX users to interpolate JSX elements into Markdown documents, making it possible to add React components into Gatsby blog posts and pages. Markdown already has certain HTML elements available out of the box and supports inline HTML—but MDX takes this a step further by allowing developers to insert JSX directly into their Markdown documents.

Getting Started with MDX

Consider the following example Markdown document (say, src/blog/blog-post.md):

# Hello MDX!

Here is a [link](https://example.com).

<figure class="figure">
  <object data="figure.svg" type="image/svg+xml"></object>
  <figcaption>An illustration of MDX in Markdown.</figcaption>
</figure>

We can write a Figure component based on this in JSX, such as the following:

import React from "react"

export const Figure = props => {
  return (
    <figure className="chart">
      <object data={props.data} type="image/svg+xml"></object>
      <figcaption>{props.caption}</figcaption>
    </figure>
  )
}

Now, we can insert this Figure component into the same Markdown document, src/blog/blog-post.md, using MDX:

import { Figure } from './components/Figure';

# Hello MDX!

Here is a [link](https://example.com).

<Figure data="figure.svg" caption="An illustration of MDX in Markdown." />

The Gatsby ecosystem makes available a starter, gatsby-starter-mdx-basic, to add MDX support instantly to a new Gatsby site. To spin up a new Gatsby site using this starter, execute the following command:

$ gatsby new gtdg-ch13-mdx gatsbyjs/gatsby-starter-mdx-basic

It’s also possible to leverage MDX within an existing Gatsby site by adding certain dependencies, by executing the following command. Remember that sourcing data in Gatsby sites from Markdown files also requires gatsby-source-filesystem, which we install here as well:

$ npm install gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react 
  gatsby-source-filesystem

Then, add the gatsby-plugin-mdx plugin to your Gatsby configuration file (gatsby-config.js):

module.exports = {
  plugins: [
    // ....
    `gatsby-plugin-mdx`,
  ],
}

To set relevant configuration options for your requirements, as outlined in Table 13-1, add an options object:

module.exports = {
  plugins: [
    // ....
    {
      resolve: `gatsby-plugin-mdx`,
      options: {
        // Options for gatsby-plugin-mdx
      }
    }
  ],
}
Table 13-1. Configuration options for gatsby-plugin-mdx
Key Default Description
extensions [".mdx"] Configure the file extensions that gatsby-plugin-mdx will process.
defaultLayouts {} Set the layout components for MDX source types.
gatsbyRemarkPlugins [] Use Gatsby-specific plugins for Remark (a Markdown processor).
remarkPlugins [] Specify Remark (a Markdown processor) plugins.
rehypePlugins [] Specify Rehype (an HTML processor) plugins.
mediaTypes ["text/markdown", "text/x-markdown"] Determine which media types are processed by MDX.
shouldBlockNodeFromTransformation (node) => false Disable MDX transformation for nodes where this function returns true.
commonmark false Use CommonMark (a Markdown specification).
Note

For more information about MDX, consult the MDX website and the MDX plugin documentation for Gatsby. For more information about performing migrations from Remark (a Markdown processor) to MDX, consult the Gatsby documentation’s guide to migrating Remark to MDX.

Creating MDX Pages

Once you have gatsby-plugin-mdx available, either through Gatsby’s own MDX starter or by installing it into an existing Gatsby site, any MDX files located within Gatsby’s src/pages directory will automatically be converted into pages. For instance, if you create a page src/pages/about.mdx, this page will be rendered by Gatsby at my-site.com/about.

You can use frontmatter in Markdown to define certain fields that will be available in Gatsby’s GraphQL API as well through the allMdx root field. For example, suppose you have an MDX file with the following frontmatter:

---
title: Hello MDX!
date: 2021-04-19
---

# Hello MDX!

You can query for this frontmatter material by issuing the following GraphQL query either in GraphiQL or any in other file in your Gatsby site that supports GraphQL queries:

query {
  allMdx {
    edges {
      node {
        frontmatter {
          title
          date(formatString: "MMMM DD, YYYY")
        }
      }
    }
  }
}

If your Markdown document is located in your src/pages directory and not programmatically generated, you can also export the query directly within the document itself, if you wish to have your GraphQL query and Markdown located in the same file:

---
title: Hello MDX!
date: 2021-04-19
---

import { graphql } from "gatsby"

# Hello MDX!

export const pageQuery = graphql`
  query {
    allMdx {
      edges {
        node {
          frontmatter {
            title
            date(formatString: "MMMM DD, YYYY")
          }
        }
      }
    }
  }
`
Warning

In MDX files, frontmatter must precede import statements referring to outside React components.

MDX also makes available frontmatter within your MDX file itself, meaning you can reference frontmatter fields from inside the MDX document, as follows:

---
title: Hello MDX!
date: 2021-04-19
---

# Hello MDX!

- Title: {props.pageContext.frontmatter.title}
- Date: {props.pageContext.frontmatter.date}

Importing Components into MDX Files

As we saw previously, we can import arbitrary React components into our Markdown documents, such as the Figure component we wrote earlier in this section:

---
title: Hello MDX!
date: 2021-04-19
---

import { Figure } from './components/figure';

# Hello MDX!

- Title: {props.pageContext.frontmatter.title}
- Date: {props.pageContext.frontmatter.date}

<Figure data="figure.svg" caption="An illustration of MDX in Markdown." />

Components used in MDX files can also be universally leveraged across a Gatsby site as shortcodes, thanks to the MDXProvider component. For instance, to make the Figure component available to all your MDX files, you could add the following inside your layout component:

import React from "react"
import { MDXProvider } from "@mdx-js/react"
import { Figure } from "./figure"

const shortcodes = { Figure }

export default function Layout({ children }) {
  return (
    <MDXProvider components={shortcodes}>{children}</MDXProvider>
  )
}

Any MDX components that are passed into MDXProvider as the components prop will be available to any MDX documents that are rendered by the provider. This means you no longer need to import the components manually within your MDX files:

---
title: Hello MDX!
date: 2021-04-19
---

# Hello MDX!

- Title: {props.pageContext.frontmatter.title}
- Date: {props.pageContext.frontmatter.date}

<Figure data="figure.svg" caption="An illustration of MDX in Markdown." />
Note

It’s also possible to use JavaScript exports directly within MDX files for other purposes, like exporting page metadata or defining a layout. For more information on these use cases, consult the Gatsby documentation’s section on using JavaScript exports in MDX. The documentation also contains information on lazy-loading components in MDX files for performance reasons.

Customizing Markdown Components

It’s possible to substitute each HTML element that MDX renders with a custom implementation as an alternative. This enables Gatsby developers to leverage a set of design system (collections of repeatable design patterns in code) components when rendering MDX files. For example, consider the following example layout component that substitutes certain elements, like <h1>, with React components that we’ve defined:

import { MDXProvider } from "@mdx-js/react"
import * as DesignSystem from "your-design-system"

export default function Layout({ children }) {
  return (
    <MDXProvider
      components={{
        // Map HTML element tag to React component
        h1: DesignSystem.H1,
        h2: DesignSystem.H2,
        h3: DesignSystem.H3,
        // Or define component inline
        p: props => <p {...props} style={{ color: "rebeccapurple" }} />,
      }}
    >
      {children}
    </MDXProvider>
  )
}

Table 13-2 lists all of the components that can be customized using the MDXProvider component.

Table 13-2. Components customizable through MDXProvider
Element Name Markdown syntax
p Paragraph (Two carriage returns)
h1 Heading 1 #
h2 Heading 2 ##
h3 Heading 3 ###
h4 Heading 4 ####
h5 Heading 5 #####
h6 Heading 6 ######
thematicBreak Thematic break ***
blockquote Blockquote
ul Unordered list *, -, or +
ol Ordered list 1.
li List item *, -, or +
table Table `---
tr Table row `This
td/th Table cell |
pre Pre ```js console.log()```
code Code `console.log()`
em Emphasis _emphasis_
strong Strong **strong**
delete Delete ~~strikethrough~~
code Inline code `console.log()`
hr Break ---
a Link https://mdxjs.com or [MDX](https://mdxjs.com)
img Image ![alt](https://mdx-logo.now.sh)

MDX is one element of Gatsby’s ecosystem that can accelerate the development of Gatsby sites by enabling the interpolation of arbitrary components into Markdown documents. This introduces much greater flexibility for content authors working in Markdown who have a working knowledge of HTML as well. In the next section, we turn our attention to schema customization, another of Gatsby’s advanced features.

Note

The gatsby-plugin-mdx plugin is compatible with all of Gatsby’s Remark plugins, including gatsby-remark-images. For more information about using Remark plugins with MDX, consult the Gatsby documentation’s guide to MDX plugins.

Schema Customization

During the build lifecycle, which we cover in detail in the final chapter of this book, Gatsby generates a GraphQL schema that enables Gatsby developers to query data from a variety of external or internal sources. This step takes place automatically during the Gatsby bootstrap.

However, there are often scenarios where some schema customization is required in order to adjust the shape of data returned by queries, or to enrich the query layer with additional functionality. In this section, we walk through Gatsby’s Schema Customization API, which allows us to customize the schema so it adheres to our needs.

Explicitly Defining Data Types

In many Gatsby sites, we need to merge data from disparate sources and schemas into a synthesized GraphQL API that can be consumed from any Gatsby component. Standardizing discrete formats into a single schema supports long-term maintenance and a more efficient developer experience. Schema customization allows us to combine distinct data types into a cohesive schema—in fact, this is what Gatsby does through its source plugins when developers query data from multiple sources through the API.

Source and transformer plugins provide a means for Gatsby to recognize disparate data types. But these plugins in and of themselves don’t necessarily reformat the data entering GraphQL according to specifications we as Gatsby developers set. Take, for example, the following three documents, which represent a blog article, a list of authors of blog articles, and a list of translators of blog articles.

The first file is an example blog article formatted in Markdown, located at src/data/article-1.md:

---
title: Article one
date: 2020-04-19
author: [email protected]
translations:
  - English
---

# Lorem ipsum

Dolor sit amet.

The second file is a JSON list of authors that includes the author of this article:

[
  {
   "name": "Baldwin",
   "firstName": "James",
   "email": "[email protected]",
   "created": "2018-09-02"
  },
  {
   "name": "Toklas",
   "firstName": "Alice",
   "email": "[email protected]",
   "created": "2016-03-23"
  }
]

The third file is a JSON list of translators, none of whom is referenced in the article:

[
  {
   "name": "wa Goro",
   "firstName": "Wangui",
   "email": "[email protected]",
   "multipleLanguages": true
  }
]

As we’ve seen in previous chapters, we can immediately begin querying the data contained in these files by leveraging the gatsby-source-filesystem source plugin in conjunction with the gatsby-transformer-remark (Markdown) and gatsby-transformer-json (JSON) transformer plugins:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/data/`,
      },
    },
    `gatsby-transformer-remark`,
    `gatsby-transformer-json`,
  ],
}

Using these plugins out of the box and configured to pull from these directories results in a Node for each of these items of, respectively, type MarkdownRemark (Markdown), AuthorJson (JSON), and TranslatorJson (JSON).

The Node interface and automatic type inference

In Gatsby’s GraphQL schema, these three data types (MarkdownRemark, AuthorJson, and TranslatorJson) are each represented using the Node interface, which defines the GraphQL fields mutually shared by Node objects that are generated by source and transformer plugins (namely id, parent, children, and several internal fields such as type). Written in GraphQL’s Schema Definition Language (SDL), the Node interface is defined as follows:

interface Node {
  id: ID!
  parent: Node!
  children: [Node!]!
  internal: Internal!
}

type Internal {
  type: String!
}

The types that are created by source and transformer plugins, like gatsby-source-filesystem, gatsby-transformer-remark, and gatsby-transformer-json, are implementations of this Node interface. For instance, the Node type TranslatorJson will be defined in the GraphQL schema as follows:

type TranslatorJson implements Node {
  id: ID!
  parent: Node!
  children: [Node!]!
  internal: Internal!
  name: String
  firstName: String
  email: String
  multipleLanguages: Boolean
}
Note

Fields whose value types end with an exclamation point represent nullability, indicating whether a field value can be null or not; they must have a non-null value.

Gatsby performs inference to determine the field types (i.e., String, Boolean, etc.)—it inspects the contents of every field and validates its type in order to translate each data shape into meaningful type definitions. Gatsby is usually quite adept at this process of automatic type inference, though it consumes significant resources in doing so and has trouble deciding between types if multiple types (e.g., String and Date) are represented in a particular field. One potential consequence of this approach is that if data sources change the way they expose data, automatic type inference suddenly fails.

Creating explicit type definitions

For this reason, it can often be preferable to create type definitions manually, explicitly defining types so that Gatsby can validate the field and inform you if there is malformed data, such as an invalid Date. Through the createSchemaCustomization Node API, Gatsby provides a means for developers to supply the GraphQL schema with explicit type definitions in gatsby-node.js using the createTypes action, which accepts as inputs type definitions written in GraphQL’s SDL:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type AuthorJson implements Node {
      created: Date
    }
  `
  createTypes(typeDefs)
}

Thanks to Gatsby’s automatic type inference capability, there is no need for us to also provide explicit type definitions for the other field types, unless we wish to. Nevertheless, there are situations where providing an unabridged type definition may be preferable, in the process opting out of Gatsby’s automatic type inference. This is especially the case for larger Gatsby sites working with substantial amounts of data where type inference begins to detrimentally impact performance.

The @dontInfer type directive in Gatsby can be used to opt out of automatic type inference for a field, but the catch is that you must provide all type definitions explicitly in order for that field to be made available for querying via the GraphQL API. For example, opting out of automatic type inference for the AuthorJson type would mean that we would need to provide a full type definition:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type AuthorJson implements Node @dontInfer {
      name: String!
      firstName: String!
      email: String!
      created: Date
    }
  `
  createTypes(typeDefs)
}

Note that Gatsby’s internal Node interface fields (id, parent, children, internal) are automatically created by Gatsby and don’t require inclusion in the unabridged type definition.

Note

There are several extensions available in GraphQL type definitions that can be used to specify the media types that are acceptable and child relations that need to be established with a parent type. For more information about these, consult the Gatsby documentation’s sections on defining media types and child relations.

Handling nested types

GraphQL fields can handle scalar values—namely String, Date, ID, Int, Float, Boolean, and JSON—but fields can also contain complex values in the form of objects. To handle these nested fields, we need to provide an explicit type definition for the nested type. For instance, we may wish to ensure that each MarkdownRemark (Markdown document) Node contains a frontmatter field known as frontmatter.translations that itself is a list of strings:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
    }
    type Frontmatter {
      translations: [String!]!
    }
  `
  createTypes(typeDefs)
}

Note that because we are specifying the Frontmatter type for the first time using this createTypes action, we cannot simply supply a bare Frontmatter definition without accounting for it in the overarching MarkdownRemark type—without that, GraphQL will have no awareness of the Frontmatter field. For this reason, Gatsby recommends as a best practice beginning from the standpoint of the types generated by source and transformer plugins when writing type definitions.

Note

Though GraphQL’s SDL provides a sufficiently concise way to author type definitions that customize the schema, the createTypes action also allows for type definitions that use Gatsby Type Builders, which can offer more leeway than SDL syntax and result in less verbosity than direct implementations of graphql-js. For more information about foreign key fields, default field values, and available extensions and directives, consult the Gatsby documentation’s sections on foreign key fields, extensions and directives, setting default field values, and creating custom extensions.

Implementing the createResolvers API

In the course of schema customization, sometimes type definitions are insufficient for requirements in Gatsby sites. For example, the GraphQL SDL and Gatsby Type Builders can handle the vast majority of use cases when it comes to adding type definitions, but more granularity is occasionally necessary in the form of custom resolvers, which allow us to specify how fields should be resolved in the GraphQL schema. This is done through implementations of the createResolvers Node API:

exports.createResolvers = ({ createResolvers }) => {
  const resolvers = {
    Frontmatter: {
      author: {
        resolve(source, args, context, info) {
          return context.nodeModel.getNodeById({
            id: source.author,
            type: "AuthorJson",
          })
        },
      },
    },
  }
  createResolvers(resolvers)
}

Implementations of the createResolvers API enable Gatsby developers to extend types with new fields without overriding the field type. Since the createResolvers function is executed near the end of the schema generation process, modification of an existing field type would require the regeneration of input types (field, sort, etc.), which would be an expensive operation. For this reason, specifying field types is best handled in implementations of the createTypes action, unless you have more advanced requirements.

While implementing the createResolvers API, Gatsby permits access to the internal data store and Gatsby’s own query capabilities through the context.nodeModel object that is made available in each resolver. In this manner, Gatsby developers can directly access nodes by their identifiers by invoking getNodeById and getNodesByIds, whereas all nodes can be retrieved through getAllNodes. You can also issue arbitrary queries from within resolver function logic through runQuery, which allows for filter and sort query arguments.

For example, the following createResolvers implementation extends the AuthorJson type with a field that lists all recent articles written by a particular author:

exports.createResolvers = ({ createResolvers }) => {
  const resolvers = {
    AuthorJson: {
      recentPosts: {
        type: ["MarkdownRemark"],
        resolve(source, args, context, info) {
          return context.nodeModel.runQuery({
            query: {
              filter: {
                frontmatter: {
                  author: { eq: source.email },
                  date: { gt: "2019-01-01" },
                },
              },
            },
            type: "MarkdownRemark",
            firstOnly: false,
          })
        },
      },
    },
  }
  createResolvers(resolvers)
}

If invoking runQuery from within the API implementation to sort results of the query, note that sort.fields and sort.order are both GraphQLList fields. Meanwhile, nested fields within sort.fields are accessed through dot notation rather than triple underscores, as we’ve seen in previous examples:

// ...
return context.nodeModel.runQuery({
  query: {
    sort: {
      fields: ["frontmatter.date"],
      order: ["DESC"],
    },
  },
  type: "MarkdownRemark",
})
// ...
Note

A powerful feature available in the createResolvers API that is beyond the scope of this overview of schema customization is custom query fields. For more information about these, consult the Gatsby documentation.

Creating Custom Interfaces and Unions

One final common use case for schema customization in Gatsby involves creating custom interfaces and unions across multiple types through GraphQL’s abstract types. For instance, we could issue two queries for allAuthorJson and allTranslatorJson and then merge these by writing Gatsby code, but GraphQL can give us these types out of the box as a merged list.

Because both the AuthorJson and TranslatorJson types have most of their fields in common, we can create an interface that merges these two together:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    interface Creator {
      name: String!
      firstName: String!
      email: String!
    }
    type AuthorJson implements Node & Creator {
      name: String!
      firstName: String!
      email: String!
      created: Date
    }
    type TranslatorJson implements Node & Creator {
      name: String!
      firstName: String!
      email: String!
      multipleLanguages: Boolean
    }
  `
  createTypes(typeDefs)
}

In addition, we can implement the createResolvers API to facilitate a new type for us, allCreator, which makes our merged list available:

exports.createResolvers = ({ createResolvers }) => {
  const resolvers = {
    Query: {
      allCreator: {
        type: ["Creator"],
        resolve(source, args, context, info) {
          return context.nodeModel.getAllNodes({ type: "Creator" })
        },
      },
    },
  }
  createResolvers(resolvers)
}

Now, we can access the fields for both AuthorJson and TranslatorJson and acquire the authors’ and translators’ email addresses as follows:

export const query = graphql`
  {
    allCreator {
      ... on AuthorJson {
        email
      }
      ... on TranslatorJson {
        email
      }
    }
  }
`

As of Gatsby 3.0, it’s possible to use interface inheritance in schema customization to achieve the same result as seen in the preceding example. Using the implements keyword we can inherit from the Node interface, which will mean that the interface will behave like a normal top-level type that implements that interface, like allAuthorJson. This means we no longer need a resolver in Gatsby 3.0 implementations, because Gatsby will automatically add the requisite root query fields on our behalf:

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    interface Creator implements Node {
      id: ID!
      name: String!
      firstName: String!
      email: String!
    }

    type AuthorJson implements Node & Creator {
      name: String!
      firstName: String!
      email: String!
      created: Date
    }

    type TranslatorJson implements Node & Creator {
      name: String!
      firstName: String!
      email: String!
      multipleLanguages: Boolean
    }
  `
  createTypes(typeDefs)
}
Warning

In Gatsby, every type that implements an interface that can be queried must also implement the Node interface.

This also means we only need to use fragments (discussed in Chapter 4) in the query for those fields that are not shared between both types:

export const query = graphql`
  {
    allCreator {
      nodes {
        name
        firstName
        email
        __typeName
        ... on AuthorJson {
          created
        }
        ... on TranslatorJson {
          multipleLanguages
        }
        ... on Node {
          parent {
            id
          }
        }
      }
    }
  }
`
Note

Schema customization in Gatsby also allows you to provide customizations that extend third-party GraphQL types that may have been supplied by remote sources, such as through the gatsby-source-graphql source plugin, by implementing the createResolvers API. For more information about this, consult the Gatsby documentation’s section on extending third-party types.

Custom Gatsby Configuration

Over the course of developing a Gatsby site, sometimes additional customization is needed to suit particular needs when it comes to how Gatsby performs a build and yields bundles for consumption. In this section, we examine custom approaches to Gatsby configuration outside of schema customization—we’ll look at using Babel, Webpack, Gatsby’s html.js file, and ESLint, as well as proxying API requests.

Babel

Gatsby leverages Babel to provide support for both older browsers and modern JavaScript development paradigms. By default, Gatsby’s Babel configuration guarantees support for the previous two versions of commonly used browsers, Internet Explorer 9+, and any other browser having more than 1% market share.

Babel in Gatsby automatically transpiles all JavaScript to ensure that all written code (including polyfills, alternative code for earlier browsers without support for certain features that are automatically added during the Gatsby build process) functions properly in older browsers. Gatsby provides a default .babelrc configuration file that supports compatibility for the vast majority of Gatsby sites. To customize this configuration file to your unique requirements, first install babel-preset-gatsby:

$ npm install --save-dev babel-preset-gatsby

Then, create a new overriding .babelrc configuration file in your Gatsby project root to include additional plugins, presets, and other settings. For example, passing the targets option object containing a browsers key with an array of specified browsers will override Gatsby’s default supported browsers according to your configuration:

// .babelrc
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ],
  "presets": [
    [
      "babel-preset-gatsby",
      {
        "targets": {
          "browsers": [">0.25%", "not dead"]
        }
      }
    ]
  ]
}

From this point forward, you can also copy certain default settings from the babel-preset-gatsby preset and override them as needed.

Note

For more information about which browsers are supported by Gatsby and other means to adjust this list, consult the Gatsby documentation’s guide to browser support.

Babel Plugin Macros

Gatsby also permits the use of Babel macros to apply compile-time code transformations that can be more flexible than Babel plugins. Rather than including them in the .babelrc configuration file, we insert Babel plugin macros into the working code of the files we write. Babel macros have two key advantages:

  • There is no uncertainty about where particular nonstandard or noncompliant syntax is originating from, as macros are explicitly imported in the places they are used.

  • There is no need for configuration files, as macros are only included in code directly on an as-needed basis.

Because Babel plugin macros only run at compile time, like Babel plugins themselves, they are unavailable in the publicly distributed JavaScript bundle. Therefore, these macros have no impact other than the transformations they are responsible for.

Like Babel plugins, many macros are available in the JavaScript ecosystem as packages. The convention for community macros is to suffix the name of the macro, typically a description of its function, with .macro. For example, preval.macro is a macro that forces the preevaluation of the code it is responsible for. We can install this macro into our development dependencies as follows:

$ npm install --save-dev preval.macro

We can then import the macro and use it with a template literal tag to perform certain logic. For example, the following code:

import preval from "preval.macro"
const y = preval`module.exports = 2`

will yield this transformed code in the resulting project build:

const y = 2
Note

For more information about Babel macros, consult the Babel plugin macros documentation. The awesome-babel-macros repository also contains useful information about macros and macro development.

Webpack

As a Gatsby developer, you should only attempt a custom Webpack configuration if the default Webpack configuration built into Gatsby does not support your requirements and there is no Gatsby plugin available in the ecosystem that meets your needs. For those cases, Gatsby provides a Node API known as onCreateWebpackConfig that can be implemented in gatsby-node.js to adjust the default Gatsby Webpack configuration.

When Gatsby generates its own Webpack configuration during the build lifecycle, it will use the webpack-merge library to combine the two configurations together appropriately. We’ll dig into the details of how Gatsby performs Webpack builds in the final chapter of this book, but for now one of the most important concepts to understand about Webpack use in Gatsby is that distinct builds are generated based on each build type or stage, of which four exist:

develop
This stage occurs when you execute the gatsby develop command, and the Webpack configuration includes settings for hot reloading and in-page CSS injection to aid development.
develop-html
This is identical to the previous stage but is responsible for rendering the HTML component, which represents the outermost component of a Gatsby site (see also the next section).
build-javascript
This stage is responsible for the production JavaScript and CSS build, including the creation of route-specific JavaScript bundles and common Webpack chunks for JavaScript and CSS assets.
build-html
This stage represents the production build that yields static HTML pages.

For an example of customizing Webpack configuration using the onCreateWebpackConfig API, consider the following code that incorporates the less-loader plugin for LESS stylesheet files requiring compilation:

exports.onCreateWebpackConfig = ({
  stage,
  rules,
  loaders,
  plugins,
  actions,
}) => {
  actions.setWebpackConfig({
    module: {
      rules: [
        {
          test: /.less$/,
          use: [
            // You don't need to add the matching ExtractText plugin
            // because Gatsby already includes it and makes sure it's only
            // run at the appropriate stages, e.g. not in development
            loaders.miniCssExtract(),
            loaders.css({ importLoaders: 1 }),
            // The postcss loader comes with some nice defaults
            // including autoprefixer for our configured browsers
            loaders.postcss(),
            `less-loader`,
          ],
        },
      ],
    },
    plugins: [
      plugins.define({
        __DEVELOPMENT__: stage === `develop` || stage === `develop-html`,
      }),
    ],
  })
}

Another common use case, particularly for repetitive references to components in import statements, is to set the Webpack configuration to permit absolute imports in order to avoid specifying paths each and every time you import the components. For instance, consider the following example API implementation in gatsby-node.js:

exports.onCreateWebpackConfig = ({ stage, actions }) => {
  actions.setWebpackConfig({
    resolve: {
      modules: [path.resolve(__dirname, "src"), "node_modules"],
    },
  })
}

Instead of writing the following in Gatsby components:

import Footer from '../../components/footer'

you can write the following after adjusting the Webpack configuration accordingly:

import Footer from 'components/footer'
Note

For more information about other advanced use cases involving the customization of Gatsby’s default Webpack configuration, consult the Gatsby documentation on importing non-Webpack tools using Yarn and modifying the Babel loader.

Customizing html.js

Gatsby uses a React component to render <head>, <footer>, and other elements that lie outside the core application destined for the browser. For the vast majority of Gatsby sites, the html.js file that comes packaged with Gatsby suits most requirements, but some situations call for additional customization. To customize your site’s default html.js, execute the following in the root of your Gatsby project:

$ gatsby build
$ cp .cache/default-html.js src/html.js

This creates an html.js file that overrides Gatsby’s own.

Tip

Customization of the html.js file is recommended by Gatsby only when the appropriate Gatsby Server-Side Rendering APIs implemented in gatsby-ssr.js are not sufficient for your needs. Before performing this customization, consider implementing the onRenderBody or onPreRenderHTML APIs instead. Plugin authors should explore using setPostBodyComponents.

Certain props found within the html.js file are required for Gatsby in order to perform rendering correctly. These are:

  • headComponents

  • preBodyComponents

  • body

  • postBodyComponents

Within the html.js file, you may need to add custom JavaScript that executes entirely outside Gatsby’s purview, to avoid Gatsby’s own JavaScript processing. To insert custom JavaScript, use React’s dangerouslySetInnerHTML attribute as follows:

<script
  dangerouslySetInnerHTML={{
    __html: `
      var name = 'world';
      console.log('Hello ' + name);
    `,
  }}
/>
Warning

If you encounter an error such as the following, your html.js file lacks a required target container in which Gatsby can perform certain actions:

Uncaught Error: _registerComponent(...):
Target container is not a DOM element.

To resolve this error, insert a <div> element with an id attribute of ___gatsby, such as the following:

<div
  key={`body`}
  id="___gatsby"
  dangerouslySetInnerHTML={{ __html: this.props.body
}}
/>

ESLint

ESLint is a powerful open source utility for JavaScript aimed at detecting malformed syntax using a type of static analysis known as code linting. Due to JavaScript’s loosely typed and dynamic nature, the language is prone to syntax errors committed by developers. ESLint allows developers to run tests across their code without executing the logic.

As with Webpack, Gatsby provides a built-in ESLint configuration, and these settings should be suitable for the vast majority of Gatsby sites. However, there may be scenarios where you need to customize the ESLint configuration to adhere to certain other requirements. To do this, first replicate the configuration that is built into Gatsby by installing necessary dependencies into your development dependencies:

$ npm install --save-dev eslint-config-react-app

Then, create a configuration file for ESLint within your Gatsby project root as follows:

$ touch .eslintrc.js
Note

When there is no ESLint configuration file available, Gatsby instead adds a rudimentary ESLint loader implicitly that is responsible for piping feedback from the ESLint checker into the terminal or console. This is done through the built-in eslint-loader utility.

Add the following initial configuration to your ESLint configuration file, after which you’ll be able to include any additional presets, plugins, and linting rules required for your unique needs:

module.exports = {
  globals: {
    __PATH_PREFIX__: true,
  },
  extends: `react-app`,
}
Warning

If you create an empty ESLint configuration file devoid of any data, this will disable ESLint for your Gatsby site, because Gatsby will assume your configuration file should override its own built-in ESLint configuration.

Proxying API Requests

The final portion of this section deals with proxying API requests, which involves customizing the Gatsby configuration file. Many Gatsby developers employ an architectural approach that involves hosting the backend server implementation and frontend React application from the same host and port, which can lead to issues when sourcing data for presentation in Gatsby, as some source plugins expect distinct hostnames or ports.

To mitigate this problem, Gatsby’s development server can be instructed through configuration to proxy any unknown incoming requests to the API server during development with the proxy field in gatsby-config.js. The proxy field accepts a single object or an array of objects, as the following example demonstrates:

module.exports = {
  proxy: [
    {
      prefix: "/api",
      url: "https://dev-my-site.com",
    },
    {
      prefix: "/api2",
      url: "https://dev2-my-site.com",
    },
  ],
}

When performing data retrieval in development, Gatsby’s development server recognizes that rather than being a request for a static asset, the request is for API data. As such, Gatsby will then proxy the API request to the designated fallback as set in the Gatsby configuration file. For instance, a request to /api/articles will be proxied to https://dev-my-site.com/api/articles.

Note

The proxy configuration in Gatsby only takes effect in the development server and has no effect in production. For more information about advanced proxying use cases like adding middleware with the developMiddleware option and self-signed certificates, consult the Gatsby documentation’s guide to advanced proxying.

Performance Optimization

The topic of performance is a broad one, and this book cannot possibly do it justice. But fortunately, Gatsby includes many performance enhancements that ensure a high-performing Gatsby site with little to no additional work. For instance, Gatsby comes prepackaged with common modern paradigms for performant websites such as link prefetching, code splitting, and other techniques.

For developers working on large-scale Gatsby sites, performance optimization is a fundamental consideration that can have outsized impacts on the success of the site in the long run. In this section, we cover caching best practices, progressive web app functionality, offline support, profiling, performance tracing, and conditional page builds, all of which concern Gatsby’s performance across the stack.

Note

Two common developer tools to analyze performance are Lighthouse, which provides performance auditing for Gatsby sites in the browser, and Guess.js, which uses machine learning to predict which pages a user will navigate to from the current page, preloading only those resources accordingly. For more information, consult the Gatsby documentation’s guides to auditing with Lighthouse and optimizing with Guess.js.

Caching Gatsby Sites

One of the foundational best practices for Gatsby static sites is to perform proper HTTP caching, which enables browsers to optimally cache resources from a given website such that the user experiences a near-instantaneous load. The Gatsby documentation outlines several types of resources in the public directory that should be cached (or not) according to a particular set of configurations:

HTML
HTML files should not be cached by the browser, because each Gatsby rebuild updates the contents of static HTML files that Gatsby generates. Browsers should be set to verify on each request whether a newer version of the HTML file needs to be downloaded.
Page data
Gatsby recommends excluding the JSON files located in the public/page-data directory from browser caching, because these files are also entirely updated on each rebuild and, in fact, can be updated without a rebuild taking place. Therefore, browsers should be set to verify on each request whether new page data is available for download.
App data
The app-data.json file includes the build hash for the most recent deployment of the Gatsby site and thus ought to share the same cache-control header as page-data.json, so that the version of application data loaded in the browser remains in sync with the current Gatsby deployment.
Static files
Files in the static directory are designed to be cached in perpetuity. Gatsby generates paths for each file in this directory that are associated with the contents of the file, such that file paths change when the file contents evolve.
JavaScript and CSS assets
Any JavaScript and CSS assets that are handled by Webpack should be cached in perpetuity as well, as they are unchanging unless their file contents change. The only exception to this best practice is the /sw.js file, which is generated by the gatsby-plugin-offline plugin (see ““Adding Offline Support with Service Workers””).

Table 13-3 outlines the cache-control headers that should be set for each of these asset types when caching Gatsby sites.

Table 13-3. Appropriate cache-control headers for different Gatsby asset types
Asset type Header
HTML cache-control: public, max-age=0, must-revalidate
Page data
App data
Static files cache-control: public, max-age=31536000, immutable
JavaScript and CSS assets
/sw.js cache-control: public, max-age=0, must-revalidate

The gatsby-plugin-netlify and gatsby-plugin-s3 plugins have automated caching headers enabled for their respective infrastructures.

Note

The following cache header can also be used for HTML, page data, and app data, because no-cache enables a cache to deliver cached content so long as it validates the freshness of the cache first:

cache-control: public, no-cache

Adding a Progressive Web App Manifest File

Progressive web apps (PWAs) are websites that can run in the browser but also leverage certain functionality to provide native application–like advantages. For those Gatsby developers who wish to enable their Gatsby sites as PWAs, the most important step is to add a manifest file, manifest.webmanifest, that indicates PWA compatibility.

The gatsby-plugin-manifest plugin can create a manifest file on your behalf. To install the plugin, execute the following commands and ensure a favicon is available at src/images/icon.png:

$ gatsby new gtdg-ch13-pwa gatsbyjs/gatsby-starter-default
$ npm install gatsby-plugin-manifest

Then, add the plugin to the plugins array in your Gatsby configuration file, as follows:

{
  plugins: [
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: "GatsbyJS",
        short_name: "GatsbyJS",
        start_url: "/",
        background_color: "#6b37bf",
        theme_color: "#6b37bf",
        // Enables "Add to Homescreen" prompt and disables browser UI
        // (including back button)
        display: "standalone",
        icon: "src/images/icon.png", // Relative to the root of the site.
        // An optional attribute which provides support for CORS check.
        // Without this attribute, it will skip CORS for manifest.
        // Any invalid keyword or empty string defaults to `anonymous`.
        crossOrigin: `use-credentials`,
      },
    },
  ]
}

Whenever you perform another build of your Gatsby site, a manifest.webmanifest file will automatically be generated for your PWA-enabled site.

Note

For more information about PWA functionality in Gatsby, consult the Gatsby documentation’s guide to Progressive Web Apps and the gatsby-plugin-manifest plugin documentation. See also Google’s PWA overview.

Adding Offline Support with Service Workers

Adding offline support with service workers is another way to improve performance, particularly because service worker usage is a PWA requirement. The gatsby-plugin-offline plugin automates the process of transforming a Gatsby site into an offline-first implementation that is resistant to inconsistent network conditions with the help of a service worker.

Service workers, in short, are scripts executed in the background by browsers that are distinct from logic run in the web page itself. In spotty network conditions, service workers can provide a better user experience in addition to supporting common features such as push notifications and synchronization of assets in the background.

To install the gatsby-plugin-offline plugin, execute the following command:

$ npm install gatsby-plugin-offline

Then, add the plugin to your Gatsby configuration file:

{
  plugins: [
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        // ...
      }
    },
    `gatsby-plugin-offline`,
  ],
}
Note

Service workers in Gatsby are only available on production-built Gatsby sites (i.e., on gatsby build).

To render a custom message in the browser once your service worker discovers updated content, you can implement one of Gatsby’s Browser APIs, onServiceWorkerUpdateReady, in gatsby-browser.js.. The following illustrates an example implementation, displaying a confirmation prompt requesting the user to approve a refresh of the page:

export const onServiceWorkerUpdateReady = () => {
  const answer = window.confirm(
    `A new version of this application is available. ` +
    `Refresh to update?`
  )

  if (answer === true) {
    window.location.reload()
  }
}

If you need to add a custom service worker to Gatsby, for example to support a requirement unsupported by the gatsby-plugin-offline plugin, add a file to the static directory named sw.js (per service worker convention) and implement the registerServiceWorker API in gatsby-browser.js as follows:

export const registerServiceWorker = () => true

To remove the service worker entirely from your built Gatsby site, use the Gatsby ecosystem plugin gatsby-plugin-remove-serviceworker.

Note

For more information about removing a service worker, consult the documentation for the gatsby-plugin-offline plugin. For more information about service workers in general, consult the documentation provided by Google and the Mozilla Developer Network.

Profiling with React Profiler

As of React 16.5, React now includes support for profiling, which is a means of capturing information with timings that assist Gatsby developers in pinpointing performance issues within a given Gatsby site. Users of React Developer Tools can access a Profiler tab to diagnose issues, and Gatsby automatically enables profiling in development, though development profiling does not match performance in production.

To enable profiling for a given production Gatsby build, execute the following command:

$ gatsby build --profile

The profiler should only be included in a Gatsby build when necessary for performance profiling, as it adds some computational and memory overhead to a Gatsby application. Though the Profiler tab will display overarching profiling results, you may wish to customize React’s profiler further to enable greater introspection into the performance of your React components.

For example, you can write a profiler component for a given “slow” component as follows:

import * as React from "react"
import { Profiler } from "react"

export const MyComponent = props => (
  // See https://reactjs.org/docs/profiler.html#onrender-callback
  // for onRender parameters
  <Profiler id={props.someUniqueId} onRender={capturePageMetrics}>
    <SlowComponent />
  </Profiler>
)

To profile Gatsby page performance, implement the wrapPageElement API in gatsby-browser.js, as demonstrated in the following example:

import * as React from "react"
import { Profiler } from "react"

export const wrapPageElement = ({ element, props }) => (
  <Profiler id={props.someUniqueId} onRender={capturePageMetrics}>
    {element}
  </Profiler>
)
Note

For more information about the React profiler, consult the introductory blog post and the React profiler documentation.

Performance Tracing for Gatsby Builds

Profiling can give you a significant amount of information about the React side of the equation, but performance tracing to see what takes the longest during Gatsby builds is important as well. Gatsby provides performance tracing capabilities compatible with the OpenTracing standard that can be viewed in an introspection tool such as Zipkin, Jaeger, or Honeycomb.

The instrumentation in Gatsby’s code is done through OpenTracing, an implementation-agnostic tracing API that requires an integrated OpenTracing-compatible library. Gatsby also offers additional tracing capabilities for GraphQL resolvers, which may have a detrimental impact on performance. As such, this is disabled by default, but it can be enabled using the --graphql-tracing flag as follows when kicking off a build:

$ gatsby build --graphql-tracing

Once you’ve added an OpenTracing-compatible library to the dependencies in your package.json file through a package installation, you will need to configure the library to set fields such as the tracing backend’s URL and the frequency with which spans should be delivered to that tracing backend, as well as the service name for recording. OpenTracing configuration files consist of two function exports:

create
This function creates and returns a tracer compatible with OpenTracing and is invoked when the build initializes.
stop
This function is invoked when the build concludes. Any cleanup that the tracer needs to perform should occur at this point, including clearing any remaining span queues and delivering them to the backend.

Once you have your OpenTracing configuration file in place, you can execute a build for tracing with the following command, optionally including --graphql-tracing:

$ gatsby build --open-tracing-config-file
Note

Coverage of tracing using Jaeger and Zipkin in Gatsby is beyond the scope of this book, but you can find out more in the Gatsby documentation. For more information about custom-built tracing, consult the Gatsby documentation’s guide to adding your own tracing. In addition, Gatsby recommends certain workarounds when scalability issues arise, such as out-of-memory errors or extremely slow builds. For more information about these, consult the documentation’s guide to scaling issues.

Conditional Page Builds

One of the key differences between Gatsby v2 and Gatsby v3 is the new stability of conditional page builds, also known as incremental builds. In Gatsby v2, this experimental functionality was made available by enabling the GATSBY_EXPERIMENTAL_PAGE_BUILD_ON_DATA_CHANGES environment variable. The core capability of conditional page builds is the ability to regenerate only those static HTML files that need to be rerendered (thus requiring the .cache and public directories from the previous build to remain in place).

To accomplish this, Gatsby tracks certain inputs that influence the generation of HTML files, including the page template being utilized by the given page, the result of each page’s page query, the results of any static queries used by a page template, and any frontend source code (especially implementations of Gatsby’s Browser and SSR APIs). When these remain unchanged from the previous build based on the contents of the .cache and public directories, previous HTML files that were generated in prior builds can be reused untouched.

Note

For more information about how to use conditional page builds, and especially one key change that obligates developers to avoid direct filesystem calls in gatsby-ssr.js, consult the relevant section in the release notes for Gatsby 3.0.

Conclusion

In this chapter, we covered a slew of advanced topics that ran the gamut from infrastructure as code with Gatsby recipes and schema customization to expert configuration and performance optimization. Though Gatsby comes equipped with many defaults that suit the vast majority of Gatsby sites, sometimes developers require more flexibility to handle more nuanced requirements or to extend existing capabilities with features like MDX. In addition, using Gatsby in production can lead to other required actions on the part of the developer in order to reconcile drift between environments and optimize performance for real-world conditions.

This chapter concludes our overview of how developers can leverage Gatsby for a wide variety of site development needs. But our exploration of Gatsby isn’t yet at a close. In the last and final chapter of this book, we’ll take a quick look behind the scenes and dig into the nuts and bolts of what makes Gatsby tick, including topics such as schema generation, page creation, query extraction and execution, writing out pages, and bundling everything together for the journey to the browser.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.220.62.183