all posts

Gatsby: Paging Tags and Posts in Categories, Part 1

Published to Blog on 7 Mar 2018

Gatsby

This is the part 1 of a couple of posts explaining how I page tags and posts by category on this Gatsby site using gatsby-pagination.

My previous blog(s) was a bit unique in that it had the concept of categories (like /blog, /some-other-blog, /gallery). This allowed you to host multiple blogs on one site, break your blog up into multiple interests, or host different types of things all under one site. For example you could have a blog at /blog, a photo gallery at /gallery, a list of projects you have worked on at /projects, etc. When migrating I chose to maintain that philosophy primarily because I wanted to keep my existing posts at the same url where they existed before, at /blog/some-post-title. I also wanted to leave open the possiblity of expanding in the future, perhaps to add an additional section or two to the site later.

Because of the use of categories I needed a page that would show posts in the category (the /blog), which was easy enough to accomplish using plenty of existing Gatsby documentation. In addition I also did not want a root-level tags page or posts in tag pages. Instead I wanted to display tags by category (/blog/tags) and posts in those category-tag combinations (/blog/tags/node). And not being demanding enough, due to carrying over nearly 500 historic blog posts, I also wanted the results for all those pages to be paged 10 results per page. As you can imagine there was not one great example that I could find that did exactly what I desired. I spent lots of time looking at nearly every example that I could find and ended up coming up with a solution that stitched bits of each of them into a good result. I thought I would share my results here with you. That is what you do on a blog, is it not?!

In this first part I will describe and show examples from the gatsby-node.js file where the pages are created. In following posts I will show the category and tag template pages and paging components.

Setting things up

First, in the onCreateNode function of gatsby-node.js, add fields to each post for category path and paths for each tag. These will help later when we are rendering links to the category and tags. The next thing added is an ids array so we can construct queries for “where post id is in this array of ids”.

if (node.internal.type === 'MarkdownRemark') {
  createNodeField({
    node,
    name: 'ids',
    value: [node.frontmatter.id]
  });

  if (node.frontmatter.tags) {
    const tagPaths = node.frontmatter.tags.map(tag => `/${_.kebabCase(node.frontmatter.category)}/tags/${_.kebabCase(tag)}/`);
    createNodeField({ node, name: 'tagPaths', value: tagPaths });
  }

  if (node.frontmatter.layout === 'post') {
    const categoryPath = `/${_.kebabCase(node.frontmatter.category)}/`;
    createNodeField({ node, name: 'categoryPath', value: categoryPath });
  }
}

Gathering Categories, Tags and Pages

Next we’ll iterate through each post/page in the CreatePages function and put together a list of each category, the ids for all posts in that category, all the tags in the category, and the ids for every post in that category having that tag.

const categories = {};
const posts = [];

result.data.allMarkdownRemark.edges.forEach(({ node }) => {
  const { category, id: postId, tags, layout } = node.frontmatter;
  posts.push(node);
  if (layout !== 'post') { return; }
  // add category and posts in category
  if (!categories[category]) {
    categories[category] = {
      posts: [],
      tags: []
    };
  }
  const thisCategory = categories[category];
  thisCategory.posts.push(postId);
  // add tags in category and posts in tag
  if (tags) {
    tags.forEach((tag) => {
      if (tag && tag !== '') {
        if (!thisCategory.tags[tag]) {
          thisCategory.tags[tag] = [];
        }
        thisCategory.tags[tag].push(postId);
      }
    });
  }
});

createTagPages(createPage, categories);
createCategoryPages(createPage, categories);
createPages(createPage, posts);

The end result will be a categories object that looks something like this:

{
  blog: {
    posts: [
      "982",
      "981",
      "980",
      /* etc */
    ],
    tags: [
      gatsby: [
        "982",
        "980",
        "741"
      ],
      node: [
        "981",
        "980",
        "888"
      ],
      javascript: [
        "981",
        "777"
      ],
      /* etc */
    ]
  },
  different-blog: {
    posts: [ /* etc */ ],
    tags: [ /* etc */ ]
  }
}

Creating Pages

I am sure that the code for creating posts and pages is much like any other Gatsby examples that you may have seen. Posts/pages can have a layout of “post”, “page” or “resume”.

const templates = {
  post: path.resolve('./src/templates/post-template.jsx'),
  page: path.resolve('./src/templates/page-template.jsx'),
  resume: path.resolve('./src/templates/resume-template.jsx'),
  tag: path.resolve('./src/templates/tag-template.jsx'),
  category: path.resolve('./src/templates/category-template.jsx')
};

const createPages = (createPage, posts) => {
  posts.forEach((post) => {
    createPage({
      path: post.frontmatter.path,
      component: templates[post.frontmatter.layout],
      context: { id: post.frontmatter.id }
    });
  });
};

Creating Category Pages

Since I only have one category of posts on this site, /blog, I do not have a page listing the categories. What I do have is a paged list of posts in the blog category showing 10 posts per page (/blog, /blog/2, /blog/3). To do so I iterate through the categories (in case I ever do add more than one) and use createPaginationPages from gatsby-pagination. pathFormatter really helps here for modifying the default path. You can use the default but I wanted the first page to simply be /blog rather than /blog/1. For the edges I am just using the array of blog post ids rather than passing a list of all the posts with all their properties.

const createCategoryPages = (createPage, categories) => {
  // For each of the categories in the post object, create a category page.
  Object.keys(categories).forEach((category) => {
    const postIds = categories[category].posts;
    createPaginationPages({
      createPage,
      pathFormatter: path => `/${_.kebabCase(category)}${path !== 1 ? '/' + path : '/'}`,
      component: templates.category,
      limit: 10,
      edges: postIds,
      context: { category }
    });
  });
};

Creating Tag Pages

The first section of code below, creating the paged list of blog posts in a category/tag (/blog/tags/node, /blog/tags/node/2, /blog/tags/node/3, /blog/tags/javascript), is very similar to the code above for posts in categories. Again it uses createPaginationPages from gatsby-pagination, pathFormatter, and the edges are only the array of post ids.

const createTagPages = (createPage, categories) => {
  Object.keys(categories).forEach((category) => {
    let allTags = [];
    // For each of the tags in the post object, create a tag page.
    Object.keys(categories[category].tags).forEach((tag) => {
      allTags.push(tag);
      const postIds = categories[category].tags[tag];
      createPaginationPages({
        createPage,
        pathFormatter: path => `/${_.kebabCase(category)}/tags/${_.kebabCase(tag)}${path !== 1 ? '/' + path : '/'}`,
        component: templates.tag,
        limit: 10,
        edges: postIds,
        context: {
          category,
          tag
        }
      });
    });

This second section of this function is something new. This creates the pages that list the tags in the category (/blog/tags, /blog/tags/2, /blog/tags/3). In this case edges are the array of unique tags in the category sorted alphabetically.

    allTags = _.uniq(allTags).sort();
    createPaginationPages({
      createPage,
      edges: allTags,
      component: templates.tag,
      limit: 10,
      context: { category },
      pathFormatter: path => `/${_.kebabCase(category)}/tags${path !== 1 ? '/' + path : '/'}`
    });
  });
};

Graphql Query in Category Template

Below is the example of using the “where id is in array of ids” filter in the category template. I will go into more details about the category template and rendering the posts in a category in the next post.

allMarkdownRemark(
  filter: { fields: { ids: { in: $nodes } } },
  sort: { order: DESC, fields: [frontmatter___date] }
){
  /* define edges here */
}

Tags Template

I will also go into more details about the tags template and rendering the tags pages in the next post. But for now the tags template is basically a combination of the Making a tags page template /tags/{tag} and Make a tags index /tags page that renders a list of all tags examples provided in the Gatsby docs. The primary difference being the filtering by ids in $nodes in the graphql query like the category template example above.

Post and Page Templates

Post and page templates are similar to what you will find for examples in the Gatsby documentation but rather than doing a qraphql query for PostBySlug I use PostById.

Stay tuned for part 2.


Dan Hounshell
Web geek, nerd, amateur maker. Likes: apis, node, motorcycles, sports, chickens, watches, food, Nashville, Savannah, Cincinnati and family.
Dan Hounshell on Twitter