Building a Personal Site with Gatsby

Part 7: Adding Tags to Blog Posts

January 11, 2019

Note: This is a 9 part post that begins here: Part 1: Introduction and Setup

Adding tags to our blog posts allow users to see related content on your blog. Gatsby has very detailed doc here, which is what we're essentially following step-by-step. We will want a /tags page listing all used tags, individual tag pages with a list of its posts, and linkable tags in blog posts.

Add tags to the frontmatter of your post markdown

src/pages/blog/hello-world.md
---
title: Hello World
date: 2018-12-29
thumbnail: "../../images/kitten-1.jpeg"
tags: ['cat', 'kitten', 'javascript']
---
// ...

Let's link our tags in src/pages/blog.js. Make sure you restart your server to grab the new tags in your markdown.

Add the 'tags' field to BlogPage GraphQL query:

src/pages/blog.js
export const pageQuery = graphql`
  query {
    allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
      edges {
        // ...
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            title
            tags            // ...
          }
        }
      }
    }
  }
`;

Then render the tags in the BlogPage component. We're importing kebabCase from lodash to create slugged paths for the (forthcoming) individual tag pages.

src/pages/blog.js
import { kebabCase } from 'lodash';import React from 'react';
import { graphql, Link } from 'gatsby';
// ...
const BlogPage = ({ data }) => {
  // ...
  return (
    <Layout>
      <div className="post-list">
        {posts.map(post => (
          <div key={post.node.id} className="post-list__item">
            // ...
            <div className="post-list__content">
              <h2>{post.node.frontmatter.title}</h2>
              {post.node.frontmatter.tags ? (                <div className="tags-container">                  <ul className="taglist">                    {post.node.frontmatter.tags.map(tag => (                      <li key={tag + `tag`}>                        <Link to={`/tags/${kebabCase(tag)}/`}>{tag}</Link>                      </li>                    ))}                  </ul>                </div>              ) : null}              // ...
            </div>
          </div>
        ))}
      </div>
    </Layout>
  );
};

To create our individual tag pages, we need to use the createPages API, so we'll dig back into gatsby-node.js. First, we'll create a tags page template at src/templates/tagsTemplate.js. We can keep it really basic until we know what we're working with.

src/templates/tagsTemplate.js
import React from 'react';
import { Link, graphql } from 'gatsby';
import Layout from '../components/layout';

const Tags = ({ data }) => {
  return (
    <Layout>
      <div>Tags</div>
    </Layout>
  );
};

export default Tags;

In gatsby-node.js, we're going to:

  1. Create an array of unique tags from our blog posts.
  2. Add the tags field to our GraphQL query.
  3. Create individual tags using the tagsTemplate we created.
gatsby-node.js
const _ = require('lodash');const path = require('path');
const { createFilePath } = require('gatsby-source-filesystem');

exports.createPages = ({ actions, graphql }) => {
  const { createPage } = actions;

  const postTemplate = path.resolve(`src/templates/postTemplate.js`);
  const tagTemplate = path.resolve(`src/templates/tagsTemplate.js`);
  return graphql(`
    {
      allMarkdownRemark(
        sort: { order: DESC, fields: [frontmatter___date] }
        limit: 1000
      ) {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {              tags            }          }
        }
      }
    }
  `).then(result => {
    if (result.errors) {
      return Promise.reject(result.errors);
    }

    const posts = result.data.allMarkdownRemark.edges;    // Creates blog posts
    posts.forEach(({ node }) => {      createPage({
        path: node.fields.slug,
        component: postTemplate,
        context: { slug: node.fields.slug }, // additional data can be passed via context
      });
    });

    // create Tags pages    // pulled directly from https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/#add-tags-to-your-markdown-files    let tags = [];    // Iterate through each post, putting all found tags into `tags`    _.each(posts, edge => {      if (_.get(edge, 'node.frontmatter.tags')) {        tags = tags.concat(edge.node.frontmatter.tags);      }    });    // Eliminate duplicate tags    tags = _.uniq(tags);    // Make tag pages    tags.forEach(tag => {      createPage({        path: `/tags/${_.kebabCase(tag)}/`,        component: tagTemplate,        context: {          tag,        },      });    });  });
};
// ...

When you restart your server, you'll see that pages have been created at /tags/[tagName]. We can populate that using the tag name passed via pageContext and the data we receive through the GraphQL query.

src/templates/tagTemplates.js
// https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/#add-tags-to-your-markdown-files
import React from 'react';
import { Link, graphql } from 'gatsby';
import Layout from '../components/layout';

const Tags = ({ pageContext, data }) => {
  const { tag } = pageContext;
  const { edges, totalCount } = data.allMarkdownRemark;
  const tagHeader = `${totalCount} post${
    totalCount === 1 ? '' : 's'
  } tagged with "${tag}"`;
  return (
    <Layout>
      <div>
        <h1>{tagHeader}</h1>
        <ul>
          {edges.map(({ node }) => {
            const { title, date } = node.frontmatter;
            const { slug } = node.fields;
            return (
              <li key={slug}>
                <Link to={slug}>
                  {title} ({date})
                </Link>
              </li>
            );
          })}
        </ul>
        <Link to="/tags">All tags</Link>
      </div>
    </Layout>
  );
};

export default Tags;

export const pageQuery = graphql`
  query($tag: String) {
    allMarkdownRemark(
      limit: 2000
      sort: { fields: [frontmatter___date], order: DESC }
      filter: { frontmatter: { tags: { in: [$tag] } } }
    ) {
      totalCount
      edges {
        node {
          fields {
            slug
          }
          frontmatter {
            title
            date(formatString: "MMMM DD, YYYY")
          }
        }
      }
    }
  }
`;

The $tag variable is passed to the GraphQL via the context field from createPage in your gatsby-node.js file.

gatsby-node.js
// Make tag pages
tags.forEach(tag => {
  createPage({
    path: `/tags/${_.kebabCase(tag)}/`,
    component: tagTemplate,
    context: {
      tag,    },
  });
});

Now we can create our Tags page. Create the file src/pages/tags.js and add the following:

src/pages/tags.js
// https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/#add-tags-to-your-markdown-files

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

import { kebabCase } from 'lodash';

import Layout from '../components/layout';
const TagsPage = ({ data }) => {
  const allTags = data.allMarkdownRemark.group;

  return (
    <Layout>
      <div>
        <h1>Tags</h1>
        <ul>
          {allTags.map(tag => (
            <li key={tag.fieldValue}>
              <Link to={`/tags/${kebabCase(tag.fieldValue)}/`}>
                {tag.fieldValue} ({tag.totalCount})
              </Link>
            </li>
          ))}
        </ul>
      </div>
    </Layout>
  );
};

export default TagsPage;

export const pageQuery = graphql`
  query {
    allMarkdownRemark(limit: 2000) {
      group(field: frontmatter___tags) {
        fieldValue
        totalCount
      }
    }
  }
`;

You should see a list of all tags used and their totals at /tags.