Learning to Programmatically Create Pages in Gatsby

Note: This is part 4 of four-parts Learning GatsbyJs series. This learning post is still in active development and updated regularly.

In the previous part 3 of the four-parts series, step-by-step procedures to install & configure Gatsby plugins, gatsby-source-filesystem and gatsby-trasformer-filesystem to the hello starter site were discussed. Use of these plugins together with GraphQL were explored to extend the site with content data & transform functionality to create index page that queries markdown files and lists blog post titles, date and excerpts. Gatsby default only permits to create full post with react page components for each posts in src/pages.

Learning series
Part 1: Learning GatsbyJS – Setup & Installation
Part 2: Understanding GatsbyJS Building Blocks
Part 3: An Overview of Gatsby Plugins & GraphQL
Part 4: Learning to Programmatically Create Pages in Gatsby (this post)

The objective of this part 4 of Gatsby learning series is to explore how Gatsby programmatically create pages from Gatsby data by mapping GraphQL query results to build individual blog posts and displaying through blog templates at run time.

Creating Pages Programmatically

In Gatsby, pages can be created and modified programmatically with Gatsby Node APIs. The Node API allows to create pages by implementing createPages API with GraphQL query for every object based on a template. Each markdown page run through a template to create a page view at the path or  slug URL, which is parsed by gatsby-transformer-remark into HTML document that can be viewed in a browser. The Gatsby APIs are implemented by exporting a function with the name of the API from gatsby-node.js.

This process is achieved in the following three steps:

  • Step 1: Create slugs fields based on markdown file name, which are added to the GraphQL query results. This step is not necessary if the source data or markdown frontmatter contains path or slug URL.
  • Step 2: Use Gatsby Node API createPages to create new pages based on GraphQL query results of markdownRemark node.
  • Step 3: Then execute createPages function to generate static page based on new file path URL and blog post template.

Gatsby docs describes modifying pages created by Gatsby core or plugins.

Step 1: Creating Post Slugs

Gatsby by default does not provide fields (eg., slugs, tags etc) which are normally created with onCreateNode API with createNodeField to add in GraphQL node thus making available for query.

With createNode API, a new field can be created with createNodeField API such as slug, tags, categories, etc., of a post. Often source files (eg., external APIs, databases etc.) or markdown files contain page path or slug URL and creating new fields might not be necessary. For example, the following markdown file does contain path/slug URL in its file header (line 2), known as frontmatter.

---
slug: '/hello-gatsby-world'
title: Hello Gatsby World
date: "2019-07-11"
author: Mary Jones
---

This is is a post example written in markdown. The top header section
within --- is called frontmatter.

Creating slug from the markdown file is not necessary, if slug or path URL is provided in the markdown file frontmatter.

The following markdown file currently does not contain file path /slug in its frontmatter. By default Gatsby provides file source from page components located src/pages only, each markdown post needs a slug to define its path URL to render in a browser.

---
title: Hello Gatsby World
date: "2019-07-11"
author: Mary Jones
---

This is is a post example written in markdown. The top header section 
within --- is called frontmatter.

In the example below, it is assumed that the markdown file, currently does not include path or slug fields. With createNode API, a new slug field of markdown files will be created to deliver at slug URL in a browser.

An example of creating slug field with createNode API, as described in the Gatsby tutorial is shown below:

// gatsby-node.js
const { createFilePath, createFileNode } = require(`gatsby-source-filesystem`);

exports.onCreateNode = ({ node, getNode, actions }) => {
    const { createNodeField } = actions
    if (node.internal.type === `MarkdownRemark`) {
        const slug = createFilePath({ node, getNode, basePath: `posts` })
        createNodeField({
            node,
            name: `slug`,
            value: slug,
        })
    }
}

Using createPages action creator (lines: 4-5) of Gatsby API, a page for each of the markdown files is created to map the GraphQL query results to run through to blog template (step 3) and create static page at the page slug URL.

After restarting the development server, the site can be viewed at the browser at localhost:8000/___graphql which provides newly created slug field in GraphQL query as shown in figure below.

Figure: Screenshot of GraphQL query showing a slug field under markdownRemark (left panel) and query result with slug URL created from the markdown file (right panel).

Additional Information: Creating slugs for pages

Step 2: Creating Pages

Gatsby pages are programmatically created by (i) querying data with GraphQL, and (ii) mapping the query results to pages (see below gatsby-notes.js update in step 4).

In this learning-note post, creating pages is followed as described in the Gatsby tutorial document.

// gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)

//create pages
exports.createPages = ({ graphql, actions }) => {
  return graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `).then(result => {
    console.log(JSON.stringify(result, null, 4))
  })
}

In the example above, Gatsby Node API createPages is used (line 2) to create new pages based on GraphQL query (line 6) results of markdownRemark node (line 8). The next step is to create a template component (see below, step 3).

Step 3: Create Blog Template component

The single page of each post is delivered in a template page component. As required by createPage action to identify template component (line 5, step 4), the following skeleton is created  at src/template/blog-post.js component, to be updated later (step 5, below).

// src/templates/blog-post.js
import React from "react"
import Layout from "../components/layout"

export default () => {  
  return (
    <Layout>
      <div>        
      <h1 className="single-entry-title">My Blog Post</h1>            
      </div>    
    </Layout>
  )
}
Step 4: Update gatsby-node.js

Then the createPages function is executed to generate static page based on new file path URL (line 4) and blog post template component (line5) . Then the gatsby-note.js is updated with path URL to the blog-post.js template (line 5) created in step 3.

// append to gatsby-node.js
    result.data.allMarkdownRemark.edges.forEach(({ node }) => {
      createPage({
        path: node.fields.slug,
        component: path.resolve(`./src/templates/blog-post.js`),
        context: {
          // Data passed to context is available
          // in page queries as GraphQL variables.
          slug: node.fields.slug,
        },
      })
    })

The createPage function provide access to createPage action (line 5, step 2) to create a page. Each page to be created requires a path URL (line 4), a page component template to render the page (line 5), and any context needed for component (eg., slug) for rendering (lines: 6-9).

Putting all together in gatsby-node.js

Putting together the gatsby-node.js code snippets described in Steps 1, 2 & 4, the complete gatsby-node.js to create a new slug field, create a page with createPages and to provide access to createPage action is shown below:

// gatsby-node.js
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {  
   const { createNodeField } = actions

   if (node.internal.type === `MarkdownRemark`) {    
     const slug = createFilePath({ node, getNode, basePath: `posts` })    
      createNodeField({      
         node,      
         name: `slug`,      
         value: slug,    
      })
    }
  }
 //createPages
  exports.createPages = ({ graphql, actions }) => {
    const { createPage } = actions
    return graphql(`    
      {      
        allMarkdownRemark {        
           edges {          
              node {            
                fields {              
                   slug            
                }          
              }        
            }      
        }    
    }  
 `
).then(result => {    
   result.data.allMarkdownRemark.edges.forEach(({ node }) => {      
      createPage({        
        path: node.fields.slug,        
        component: path.resolve(`./src/templates/blog-post.js`),        
        context: {          
           // Data passed to context is available          
           // in page queries as GraphQL variables.          
           slug: node.fields.slug,        
        },      
      })    
   })  
  })
}

Next step is to update the src/template/bog-post.js template component (step 3) referred in line 37.

Step 5: Update Blog Post Template

The source plugin makes available markdown files to Gatsby data thus making GraphQL available to query markdownRemark node (a property) from markdown files (lines: ). The results of the query serves as props (data) to a blog template component to view as single blog post.

An example src/template/blog-post.js is shown below:

// src/templates/blog-post.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"

export default ({ data }) => {  
  const post = data.markdownRemark 
    return (
      <Layout>
       <div>        
        <h1 className="single-entry-title">{post.frontmatter.title}</h1>        
        <div dangerouslySetInnerHTML={{ __html: post.html }} />      
       </div>    
    </Layout>
)
}
//graphQL query
export const query = graphql`  
  query($slug: String!) {    
    markdownRemark(fields: { slug: { eq: $slug } }) {      
       html      
       frontmatter {        
         title      
        }    
    }  
  }
`

The slug value is specified in createPage context in template GraphQL query (line 9, step 4). As a result of the query, html, title from matching markdownRemak file are passed to the component as props.

Step 6: Update & Link New Pages Index.js

Finally, the src/pages/index.js is updated to link with the page fields.slug URL (lines: 13-15).

// src/pages/index.js
import React from "react"
import { Link, graphql } from "gatsby"
import Layout from "../components/layout"

export default ({ data }) => (
   <Layout>
     <div>
      <h4>Total {data.allMarkdownRemark.totalCount} Posts</h4>
        {data.allMarkdownRemark.edges.map(({ node }) => (
          <div key={node.id}>
             <p className="date">Posted on {node.frontmatter.date}</p>
            <Link to={node.fields.slug} >
             <h2 className="entry-title">{node.frontmatter.title}{" "}</h2>
             </Link>
              <p className="excerpt">{node.excerpt}</p> 
          </div>
        ))}
    </div>
   </Layout>
)
//graphql query
export const query = graphql`
query {
  allMarkdownRemark (sort: {order: DESC, fields: [frontmatter___date] } ) {
    totalCount
    edges {
      node {
        id
        frontmatter {
          title
          date
        }
        fields {
          slug
        }
        excerpt
      }
    }
  }
}
`

That is it! Now the each list of posts is linked with their corresponding markdown file slug URL (lines: 13-15) and can be viewed in a browser.

View the Site in a Browser

To view the site, the development server needs to be fresh started and the site can be viewed at localhost:8000 in a browser (shown below).

Figure: Screenshot showing list of markdown files (left panel) and their corresponding full post displayed in a browser (right panel).
Wrapping Up

In this learning-note post, step by step procedure to programmatically create pages from Gatsby data by mapping GraphQL query results were discussed. Currently, this blog post is bare-bone and without adding other functionalities, like pagination, images, tags & categories, styling and typography it is still incomplete.Adding functionalities will be covered in the next learning-note post series.

Next: Learning to Add Functionality to Gatsby Site

Useful resources

While preparing this post, I have referred the following references extensively. Please to refer original posts for more detailed information.