Photo: Water, splashing
Photo by isaac sloman on Unsplash

Lately, I've been working hard on generating content for my site. So naturally, it falls into multiple categories.

I found myself copy-pasting pages and changing titles, descriptions, and GraphQL queries, a bit too much for my liking.

I didn't feel inspired!

And decided to do something better...

Here's what I did to dynamically generate category pages using a common template.

Create category metadata in gatsby-config.js

First, I defined my categories in gatsby-config.js's siteMetadata array.

  "categories": [
    {
      "title": "Blog",
      "href": "/blog",
      "description": "My blog. I write about my journey as a software engineering lead and content creator.",
      "pageHeading": "From the blog",
      "pageSubtitle": "Explore some of my blog posts as I embark on a journey to build my site from scratch using GatsbyJS and TailwindUI."
    },
    ...
  ]

Dynamically create pages in gatsby-node.js

Then, I extended Gatsby's page creation API:

exports.createPages = async ({ graphql, actions, reporter }) => {
  await createCategoryPages({ graphql, actions, reporter }); // a new async function I created
};

I split various functionality by context. I defined one function responsible for creating categories.

async function createCategoryPages({ graphql, actions, reporter }) {
  const { createPage } = actions;

  // logic here, see below
}

I loaded the site's metadata in the function's body; I needed information about my categories and the author.

  const result = await graphql(
    `
      {
        site {
          siteMetadata {
            categories {
              title
              href
              description
              pageHeading
              pageSubtitle
            }
            author {
              name
              href
            }
          }
        }
      }
    `,
  );

Next, a validation step. I have skipped the validation code, but basically, it's a null check on these objects, followed by a call to reporter.warn or reporter.panicOnBuild.

  const siteMetadata = result.data.site?.siteMetadata;
  const categories = siteMetadata?.categories;
  const author = siteMetadata?.author;

Since the component doesn't change, I cached it before iterating.

  const component = path.resolve(`./src/components/category.js`);

Finally, I iterated through all my categories to create each page.

  categories.forEach((page) => {
    createPage({
      path: page.href,
      component,
      context: {
        ...page,
        author,
      },
    });
  });

Note: the component will need to load a list of articles for each category. Since the site has articles in multiple categories, we need to pass something to construct the correct query. In Gatsby, this is achieved by passing data via the page's context. GraphQL page queries can make use of any variables passed by context.

Using a variable to filter articles by category

For my site, I chose to filter by each category's title. I could have easily filtered by the category's href, but I don't plan on changing either much. Time will tell if this was the right choice...

I updated the old query:

query {
    allMarkdownRemark(
        sort: { fields: [frontmatter___date], order: DESC }
        filter: {
          frontmatter: {
            category: {
              title: { eq: "A category title" }
            }
          }
        }
    ) {
        nodes {
        ...
        }
    }
}

by giving it a name and passing the $title variable.

This variable gets automatically populated from the page's context - 🪄 magic 🪄!

query CategoryQuery($title: String!) {
    allMarkdownRemark(
        sort: { fields: [frontmatter___date], order: DESC }
        filter: {
          frontmatter: {
            category: {
              title: { eq: $title }
            }
          }
        }
    ) {
        nodes {
        ...
        }
    }
}

All I had left to do was to load the page's data and populate my template:

export default function Page({ data, pageContext }) {
  const {
    title,
    description,
    pageHeading,
    pageSubtitle,
    author,
  } = pageContext;
  const posts = data.allMarkdownRemark.nodes;
  return (
    <>
        ...
    </>
  );
}

Et voila! Dynamically generated pages:

If you enjoyed this post, please share it with your friends!