Dynamic pages

In previous chapters, we learned how:

  • Create static pages
  • Fetch data from external sources
  • Query data and show it in pages

We could potentially build our static websites just with that information.

The only problem is that we need to manually create every single page that we need.

The last missing part in building a static website with GatsbyJS is to programmatically generate these pages from data.

As we have seen in the Pages chapter, at build time GatsbyJS maps every page with an URL that is its filename. If we want to programmatically generate static pages, we are not going to have different page components (one for each node), but only GraphQL nodes. We need to provide some additional information in nodes to allow GatsbyJS to generate the correct pages and URLs.

In particular we need to add a “slug” or “path” attribute to the node.

Note

A lot of source plugins automatically add this information in nodes, like CMS plugins.

The GatsbyJS building stack has a sequence of steps that perform different actions:

  • Read the configuration to load the list of plugins
  • Initialize the cache (to avoid re-fetch untouched data)
  • Pull the data and preprocess it in a GraphQL schema
  • Create pages (from /pages folder or from plugins)
  • Extract and run GraphQL queries and replace their values in pages
  • Write out the pages as static HTML pages

GatsbyJS provides a rich set of “lifecycle APIs” to hook into every step and perform some customizations. In this chapter we are going to use two of these APIs, which are the most used in plugins:

  • onCreateNode: called by GatsbyJS whenever a node is created or updated, so we can edit the current node before storing it into GraphQL
  • createPages: step that creates a page.

To implement an API, we need to export a function with the same name of the API in a file called gatsby-node.js.

Let us start with the first one, and export a function called onCreateNode:

exports.onCreateNode = ({ node }) => {
    console.log(node.internal.type)
}

This function is called whenever a node is created.

If we restart the server now, we will see that in the console we will have the types of every node. We want to add a slug only for nodes generated by the MarkdownRemark plugin.

We need to filter them by type. To generate the slug for this node, we could use a helper funcion from gatsby-source-filesystem that is made for this purpose: createFilePath.

Note

If you remember, Remark nodes are built on top of filesystem nodes.

Finally we need to add the slug attribute to the node. Nodes can be directly modified only by the plugins that created them. Other plugins can add new fields to the nodes only with a specific function called createNodeField (for security reasons).

And finally, our onCreateNode function will be similar to this:

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: `blog` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

If we restart the server and try to query the data, we will see the slug under the fields attribute.

Now that we have all the information, we need to create pages.

As mentioned in the introduction, to create a page we need to query data with GraphQL and then map the results into a page.

To do this, we need to export the other mentioned function (createPages) in gatsby-node.js file:

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

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

exports.createPages = ({ graphql, actions }) => {
    const { createPage } = actions
    return new Promise((resolve, reject) => {
      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: {
              slug: node.fields.slug,
            },
          })
        })
        resolve()
      })
    })
  }

What can we see here?

First of all we perform a GraphQL query, and we iterate through the results to create a new page.

The method createPage is a helper method that GatsbyJS uses to generate dynamic pages.

It takes 3 parameters:

path: the slug value.

This is used to generate the URL where we can access the current page.

component: the template used to populate a blog post page.

It is similar to a page component (we will see it shortly).

context: we can pass a list of variables that can be used by the queries into page components (not StaticQuery) to fetch information about the current node.

A this point we create the blog-post.js template file to end our setup:

import React from "react"
import { graphql } from "gatsby"
import Layout from '../components/layout'

export default ({ data }) => {
  const post = data.markdownRemark
  return (
    <Layout>
      <div>
        <h1>{post.frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
     HTML
      frontmatter {
        title
      }
    }
  }
`

This is similar to a simple page component, except for GraphQL query. We need to fetch data for a specific node. To do this, we can use the slug value to filter only desired node.

Note

We can filter with almost every node attribute, but it is always better use uniques values like id or slug.

Note

dangerouslySetInnerHTML is a helper function of ReactJS that allows to insert some not-reactish HTML into a component.

If we restart the server, we can now directly access the pages that were automatically created.

Note

To easily get a list of generated URLs, try to access a random page like http://localhost:8000/asdf. The default NotFound page will offer alternative URLs.

Last thing that we could do is to link them in our index.js page:

import React from 'react'
import { Link } from 'gatsby'
import { graphql } from 'gatsby'

import Layout from '../components/layout'

const IndexPage = ({ data }) => (
    <Layout>
      <h1>A blog about The conference</h1>
      <h4>{data.allMarkdownRemark.totalCount} Posts</h4>
      {data.allMarkdownRemark.edges.map(({ node }) => (
        <div key={node.id}>
          <Link to={node.fields.slug}>
            <h3>
              {node.frontmatter.title}{" "}
              <span>
                {"- "}{node.frontmatter.date}
              </span>
            </h3>
          </Link>
          <p>{node.excerpt}</p>
        </div>
      ))}
    </Layout>
  )

export const query = graphql`
  query {
    allMarkdownRemark {
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          excerpt
          fields {
            slug
          }
        }
      }
    }
  }
`

export default IndexPage