Gatsby runs GraphQL queries at build time rather than against a live server, so query results are baked into components and shipped to the browser as static files. The framework can also expose a server that other GraphQL clients, such as GraphiQL, can query in the browser.
Two integration points matter: the gatsby-node.js API file and page components. A third path handles everything else. The createPage API takes a function whose first argument includes a graphql helper.
// gatsby-node.js source: https://www.gatsbyjs.org/docs/node-apis/#createPages
exports.createPages = async ({ graphql, actions }) => {
const result = await graphql(`
query loadPagesQuery ($limit: Int!) {
allMarkdownRemark(limit: $limit) {
edges {
node {
frontmatter {
slug
}
}
}
}
}`)
}
Here the helper fetches markdown files from Gatsby's data layer, and the resolved values can drive page creation or modify existing nodes in that layer.
Page components inside /pages, or templates rendered by createPage, import graphql from the gatsby module and export a pageQuery. Gatsby injects a data prop holding the resolved result.
import React from "react";
import { graphql } from "gatsby";
const Page = props => {
return {JSON.stringify(props.data)}
;
};
export const pageQuery = graphql`
query { ... }
`;
export default Page;
Other components import graphql and StaticQuery from the gatsby module, then render <StaticQuery/> with a query prop to receive the returned data.
import React from "react";
import { StaticQuery, graphql } from "gatsby";
const Brand = props => {
return (
<div>
<h1>{data.site.siteMetadata.title}</h1>
</div>
);
};
const Navbar = props => {
return (
<StaticQuery
query={graphql`
query {
site {
siteMetadata {
title
}
}
}
`}
render={data => <Brand data={data} {...props} />}
/>
);
};
export default Navbar;
Serving Data Without A Server
GraphQL emerged in 2015 as an alternative to REST, which had been the standard way to interface with an API. Gatsby, as a static site generator, stores static files, which makes querying by itself close to impossible. Pages such as a single blog post still need dynamic content, so data has to be pulled from a source and transformed into the needed shape—blog posts stored in markdown, for instance—and plugins supply data from a variety of sources.
Per Gatsby's own documentation, GraphQL helps by eliminating boilerplate, pushing frontend complexity into queries, handling the complex data of modern applications, and removing code bloat to improve performance. Because Gatsby executes queries at build time, data reaches components during the build and is available in the browser without a backing server.
Schema Definition Language
GraphQL embeds a type system, SDL, for declaring new types over your data. A country type might carry a name, continent, population, gdp and number of states. In the example below a type named Aleem is declared with an optional array of string hobbies, while country, marital status and posts are required—indicated by the !—and posts references another type, Post.
type Author {
name: String!,
hobbies: [String]
country: String!
married: Boolean!
posts: [Post!]
}
type Post {
title: String!
body: String!
}
type Query {
author: Author
}
schema {
query: Query
}
Queries, Arguments And Nested Fields
Queries pull data from a GraphQL source. Given a dataset such as the one below, a query can fetch the country and posts.
{
data: {
author: [
{
hobbies: ["travelling", "reading"],
married: false,
country: "Nigeria",
name: "Aleem Isiaka",
posts: [
{
title: "Learn more about how to improve your Gatsby website",
},
{
title: "The ultimate guide to GatsbyJS",
},
{
title: "How to start a blog with only GatsbyJS",
},
],
},
],
},
};
query {
authors {
country,
posts {
title
}
}
}
The response contains JSON blog posts with only the title field.
[
{ country: “Nigeria”, posts: [{...}, {...}, {...}] },
{ country: “Tunisia”, posts: [] },
{ title: “Ghana”, posts: []},
]
Arguments act as conditions on a query.
query {
authors (country: “Nigeria”) {
country,
posts {
title
}
}
}
[
{ country: “Nigeria”, posts: [{...}, {...}, {...}] }
]
Nested fields can be selected too, so asking only for the post titles returns every Author type matching Nigeria, with country and a posts array whose objects carry just the title field.
query {
authors(country: ‘Nigeria’) {
country,
posts {
title
}
}
}
Generating Post Pages
Rendering a post's content requires a template component plus a page created programmatically inside the gatsby-node.js API file. The component lives at src/templates/singlePost/index.jsx.
import React from "react";
import { graphql, Link } from "gatsby";
import _ from "lodash";
import Layout from "../../layout";
import "./b16-tomorrow-dark.css";
import "./index.css";
import PostTags from "../../components/PostTags";
export default class PostTemplate extends React.Component {
render() {
const { data, pageContext } = this.props;
const { slug } = pageContext;
const postNode = data.markdownRemark;
const post = postNode.frontmatter;
if (!post.id) {
post.id = slug;
}
return (
<Layout>
<div>
<div>
<h1>{post.title}</h1>
<div className="category">
Posted to{" "}
<em>
<Link
key={post.category}
style={{ textDecoration: "none" }}
to={`/category/${_.kebabCase(post.category)}`}
>
<a>{post.category}</a>
</Link>
</em>
</div>
<PostTags tags={post.tags} />
<div dangerouslySetInnerHTML={{ __html: postNode.html }} />
</div>
</div>
</Layout>
);
}
}
/* eslint no-undef: "off" */
export const pageQuery = graphql`
query BlogPostBySlug($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
timeToRead
excerpt
frontmatter {
title
date
category
tags
}
fields {
slug
date
}
}
}
`;
The template relies on a GraphQL helper to pull a page by slug, which arrives as a variable passed through the createPages API.
The corresponding code is appended to gatsby-node.js at the end of the createPages function.
// Template to use to render the post converted HTML
const postPage = path.resolve("./src/templates/singlePost/index.jsx");
// Loops through all the post nodes
postsEdges.forEach((edge, index) => {
// Create post pages
createPage({
path: edge.node.fields.slug,
component: postPage,
context: {
slug: edge.node.fields.slug,
},
});
});
Visiting /{pageSlug} then renders the markdown file's converted HTML. For instance, https://localhost:8000/the-butterfly-of-the-edge loads the HTML for the markdown at content/2020_05/01.md, and the same holds for every valid slug.
Category and Tag Listings
The single post template links to /categories/{categoryName} so readers can see posts sharing a category. Categories and tags are collected while each single post page is built in gatsby-node.js; a page is then created for every category and tag caught, with the category or tag name passed along. A modification to the single post section in gatsby-node.js looks like this:
const categorySet = new Set();
const tagSet = new Set();
const categoriesListing = path.resolve(
"./src/templates/categoriesListing/index.jsx"
);
// Template to use to render posts based on categories
const tagsListingPage = path.resolve("./src/templates/tagsListing/index.jsx");
// Loops through all the post nodes
postsEdges.forEach((edge, index) => {
// Generate a list of categories
if (edge.node.frontmatter.category) {
categorySet.add(edge.node.frontmatter.category);
}
// Generate a list of tags
if (edge.node.frontmatter.tags) {
edge.node.frontmatter.tags.forEach((tag) => {
tagSet.add(tag);
});
}
// Create post pages
createPage({
path: edge.node.fields.slug,
component: postPage,
context: {
slug: edge.node.fields.slug,
},
});
});
In the tag listing component, a pageQuery export queries GraphQL for posts whose tag list includes the incoming tag, using GraphQL's filter function together with the $in operator.
// src/templates/tagsListing/
import React from "react";
import { graphql } from "gatsby";
import Layout from "../../layout";
import PostListing from "../../components/PostListing";
export default ({ pageContext, data }) => {
const { tag } = pageContext;
const postEdges = data.allMarkdownRemark.edges;
return (
<Layout>
<div className="tag-container">
<div>Posts posted with {tag}</div>
<PostListing postEdges={postEdges} />
</div>
</Layout>
);
};
/* eslint no-undef: "off" */
export const pageQuery = graphql`
query TagPage($tag: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { tags: { in: [$tag] } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
author
date
}
}
}
}
}
`;
The categories listing component uses the same process; the only difference is that it matches categories exactly against the value passed to it.
// src/templates/categoriesListing/index.jsx
import React from "react";
import { graphql } from "gatsby";
import Layout from "../../layout";
import PostListing from "../../components/PostListing";
export default ({ pageContext, data }) => {
const { category } = pageContext;
const postEdges = data.allMarkdownRemark.edges;
return (
<Layout>
<div className="category-container">
<div>Posts posted to {category}</div>
<PostListing postEdges={postEdges} />
</div>
</Layout>
);
};
/* eslint no-undef: "off" */
export const pageQuery = graphql`
query CategoryPage($category: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { category: { eq: $category } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
author
date
}
}
}
}
}
`;
Both components also render links back to the single post page so a reader can continue into the post's content.
Supporting Multiple Authors
Multiple authors require changes to the post content and the introduction of a couple of new concepts.
Loading JSON Author Files
Author data is stored as JSON, for example:
{
"mdField": "aleem",
"name": "Aleem Isiaka",
"email": "[email protected]",
"location": "Lagos, Nigeria",
"avatar": "https://api.adorable.io/avatars/55/[email protected]",
"description": "Yeah, I like animals better than people sometimes... Especially dogs. Dogs are the best. Every time you come home, they act like they haven’t seen you in a year. And the good thing about dogs... is they got different dogs for different people.",
"userLinks": [
{
"label": "GitHub",
"url": "https://github.com/limistah/modern-gatsby-starter",
"iconClassName": "fa fa-github"
},
{
"label": "Twitter",
"url": "https://twitter.com/limistah",
"iconClassName": "fa fa-twitter"
},
{
"label": "Email",
"url": "mailto:[email protected]",
"iconClassName": "fa fa-envelope"
}
]
}
These files sit in an /authors directory at the root of the project. Each author JSON carries an mdField, the unique identifier referenced from the markdown blog content, which allows an individual author to hold multiple profiles.
The next step updates gatsby-config.js so that the gatsby-source-filesystem plugin loads content from the authors/ directory into the Files Node.
// gatsby-config.js
{
resolve: `gatsby-source-filesystem`,
options: {
name: "authors",
path: `${__dirname}/authors/`,
},
}
Finally, install gatsby-transform-json to transform the created JSON files, making them easier to handle and process properly.
npm install gatsby-transformer-json --save
Include it in the plugins array of gatsby-config.js.
module.exports = {
plugins: [
// ...other plugins
`gatsby-transformer-json`
],
};
Author Pages and Their Posts
All authors loaded into the data layer are queried inside gatsby-config.js by appending the following to the createPages API function.
const authorsListingPage = path.resolve(
"./src/templates/authorsListing/index.jsx"
);
const allAuthorsJson = await graphql(`
{
allAuthorsJson {
edges {
node {
id
avatar
mdField
location
name
email
description
userLinks {
iconClassName
label
url
}
}
}
}
}
`);
const authorsEdges = allAuthorsJson.data.allAuthorsJson.edges;
authorsEdges.forEach((author) => {
createPage({
path: `/authors/${_.kebabCase(author.node.mdField)}/`,
component: authorsListingPage,
context: {
authorMdField: author.node.mdField,
authorDetails: author.node,
},
});
});
The snippet pulls every author from the allAuthorsJson type, then calls forEach over the nodes to create a page. The mdField is passed to distinguish the author, and authorDetails to carry the author's full information.
The page itself is rendered by the component at src/templates/authorsListing/index.jsx.
import React from "react";
import { graphql } from "gatsby";
import Layout from "../../layout";
import PostListing from "../../components/PostListing";
import AuthorInfo from "../../components/AuthorInfo";
export default ({ pageContext, data }) => {
const { authorDetails } = pageContext;
const postEdges = data.allMarkdownRemark.edges;
return (
<Layout>
<div>
<h1 style={{ textAlign: "center" }}>Author Roll</h1>
<div className="category-container">
<AuthorInfo author={authorDetails} />
<PostListing postEdges={postEdges} />
</div>
</div>
</Layout>
);
};
/* eslint no-undef: "off" */
export const pageQuery = graphql`
query AuthorPage($authorMdField: String) {
allMarkdownRemark(
limit: 1000
sort: { fields: [fields___date], order: DESC }
filter: { frontmatter: { author: { eq: $authorMdField } } }
) {
totalCount
edges {
node {
fields {
slug
date
}
excerpt
timeToRead
frontmatter {
title
tags
author
date
}
}
}
}
}
`;
Here the exported pageQuery builds a GraphQL query fetching posts matched by an author; the $eq operator performs that match, and links are generated to the single post page for further reading.
References
- Gatsby support for GraphQL
- Why Gatsby uses GraphQL
- GraphQL concepts in Gatsby
- How To GraphQL: Basic Concepts
- Schema Definition Language In GraphQL
- An Introduction To GraphQL
- Gatsby Advanced Starter
Any data present in Gatsby's data access layer can be queried with GraphQL, and variables can be passed around using constructs defined by Gatsby's architecture. The graphql helper appears in several places, and the patterns shown here cover the common ways of querying data in Gatsby sites.
GraphQL is capable of more, including data mutation on a server, but Gatsby never needs to update its data at runtime and therefore does not support mutation. The querying side alone makes GraphQL a natural fit for the framework.




