Zacchary Puckeridge
ArticlesAbout

🎫 How to add tags to Gatsby

May 16, 2021
10 min read 1857 words

Adding tags to our Gatsby Blog will allow users to see other content associated to the post they are viewing or category they are interested in. The Gatsby Documentation has very detailed document on setting up tags here, which we are using as a guide.

We will be creating a separate tags page which will allow end users to view all the tags active on the site and their assigned posts.

Adding Tags to your Markdown Files

We can add tags to our blog posts simply by adding another line in the frontmatter of our markdown file. The frontmatter of a markdown file is the area at the top which includes post data like the time and date of publishing.

We can add our tags to our blog posts like so:

---
title: "🎫 Add Tags to your Gatsby Blog"
tags: Technical
---

Blog Post content...
Make sure you restart your server so that it has a chance to grab the new tags in your markdown!

Adding Tags to the BlogPage GraphQL Query

We need to ensure that we add the line tags to our frontmatter so that our GraphQL can query for the result.

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      nodes {
        fields {
          slug
        }
        frontmatter {
          date(formatString: "MMMM DD, YYYY")
          title
          description
          tags
        }
      }
    }
  }
`

Tags Page Template

Let’s setup a page for our end users to view all of our tags and their associated posts.

First, you’ll need to add a new file to src/templates/ called tags.js.

I’ve included a basic template below to create a page using the gatsby-starter-blog theme.

import React from "react"
import { Link, graphql } from "gatsby"

import Layout from "../components/layout"
import Seo from "../components/seo"

class TagsTemplate extends React.Component {
  render() {
    const siteTitle = this.props.data.site.siteMetadata.title
    const { blogPosts, totalCount } = this.props.data.allMarkdownRemark
    const currentTag = this.props.pageContext.tag
    const postsCounter = `${totalCount} post${
      totalCount === 1 ? "" : "s"
    } tagged with "${currentTag}"`

    return (
      <Layout location={this.props.location} title={siteTitle}>
        <Seo title={currentTag} />
        <h1>Tag: {currentTag}</h1>
        <p>{postsCounter}</p>

        <ul>
          {blogPosts.map(({ node }) => {
            const { slug } = node.fields
            const { title } = node.frontmatter
            return (
              <li key={slug}>
                <Link to={slug}>{title}</Link>
              </li>
            )
          })}
        </ul>
        <Link to="/tags">View all tags</Link>
      </Layout>
    )
  }
}

export default TagsTemplate

export const pageQuery = graphql`
  query($tag: String) {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(
      limit: 2000
      sort: { fields: [frontmatter___date], order: DESC }
      filter: { frontmatter: { tags: { in: [$tag] } } }
    ) {
      totalCount
      blogPosts: edges {
        node {
          fields {
            slug
          }
          frontmatter {
            title
          }
        }
      }
    }
  }
`

Configuring gatsby-node.js to Render Pages

Now it’s time for us to ensure that gatsby-node.js knows what it needs to do with our template page.

We will:

  • Add the tags field to the GraphQL Query
  • Create invididual tags using the tag template
  • Include the lodash library

We will be using kebabCase to create our slugged paths for unique tag pages, hence why we need to use a library called lodash.

I’ve included my gatsby-node.js file as an example below:

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

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

  // Define Page Templates
  const blogPost = path.resolve(`./src/templates/blog-post.js`)
  const tagTemplate = path.resolve(`./src/templates/tags.js`)

  // Sort Markdown Posts by Date
  const result = await graphql(`
    {
      allMarkdownRemark(
        sort: { fields: [frontmatter___date], order: ASC }
        limit: 1000
      ) {
        nodes {
          id
          fields {
            slug
          }
          frontmatter {
            tags
          }
        }
      }
      tagsGroup: allMarkdownRemark(limit: 2000) {
        group(field: frontmatter___tags) {
          fieldValue
        }
      }
    }
  `)

  if (result.errors) {
    reporter.panicOnBuild(
      `An error occurred while loading the blog posts.`,
      result.errors
    )
    return
  }

  const posts = result.data.allMarkdownRemark.nodes

  // Create blog posts pages
  // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
  // `context` is available in the template as a prop and as a variable in GraphQL

  if (posts.length > 0) {
    posts.forEach((post, index) => {
      const previousPostId = index === 0 ? null : posts[index - 1].id
      const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id

      createPage({
        path: post.fields.slug,
        component: blogPost,
        context: {
          id: post.id,
          previousPostId,
          nextPostId,
        },
      })
    })
  }

  // Extract tag data from query.
  const tags = result.data.tagsGroup.group

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

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

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })

    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions

  // Explicitly define the siteMetadata {} object
  // This way those will always be defined even if removed from gatsby-config.js

  // Also explicitly define the Markdown frontmatter
  // This way the "MarkdownRemark" queries will return `null` even when no
  // blog posts are stored inside "content/blog" instead of returning an error
  createTypes(`
    type SiteSiteMetadata {
      author: Author
      siteUrl: String
      social: Social
    }
    type Author {
      name: String
      summary: String
    }
    type Social {
      twitter: String
    }
    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
      fields: Fields
    }
    type Frontmatter {
      title: String
      description: String
      date: Date @dateformat
    }
    type Fields {
      slug: String
    }
  `)
}

Creating an Index Page for Tags

Let’s create a page designed to list all of our tags along with the number of posts associated with that tag. To do so, we need to create a new file at src/pages/ called tags.js with the following code:

Feel free to update the filenames if you get confused! Just be aware, you'll need to update the import code to ensure that there are no conflicts.
import React from "react"
import { Link, graphql } from "gatsby"

import Layout from "../components/layout"
import Seo from "../components/seo"

import kebabCase from "lodash/kebabCase"

class TagsPage extends React.Component {
  render() {
    const { data } = this.props
    const siteTitle = data.site.siteMetadata.title
    const pageTitle = "Tags"
    const tags = data.tagsGroup.group

    return (
      <Layout location={this.props.location} title={siteTitle}>
        <Seo title={pageTitle} />

        <h1>{pageTitle}</h1>

        <ul>
          {tags.map(tag => (
            <li key={tag.fieldValue}>
              <Link to={`/tags/${kebabCase(tag.fieldValue)}/`}>
                {tag.fieldValue} ({tag.totalCount})
              </Link>
            </li>
          ))}
        </ul>
      </Layout>
    )
  }
}

export default TagsPage

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

Creating the Tag Component

Now that we’ve got ourselves a Tag Index, it’s time to create our tag component, the piece of code we’ll use to put wherever we want in our pages.

We’ll need to create a file called tags.js in src/components/. I’ve included the code that I used for my blog below:

import React from "react"
import { Link } from "gatsby"

import { kebabCase } from 'lodash';

const Tags = ({ children }) =>
  children && (
    <ul style={{ marginBottom: 0, marginLeft: 0, display: "inline-flex" }}>
      {children.split(", ").map(t => (
        <li key={t} className="tag-style">
        <Link to={`/tags/${kebabCase(t)}/`}>
          {t}
        </Link>
        </li>
      ))}
    </ul>
  )

export default Tags

Effectively, we are assigning some styles and formatting to our tag. We’re also using kebabCase to set the URL of our tag.

To do this, we are pulling the tag name and putting that directly in place of the URL. Since we’ve already generated our tag pages, this is the most painless solution.

Importing Tag Component

Now we have our Tag Component, we can import it into our blog.js page and blog-post.js template like so:

Blog Index

import * as React from "react"
import { Link, graphql } from "gatsby"

import Layout from "../components/layout"
import Seo from "../components/seo"
import Tags from "../components/tags"

const BlogIndex = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`
  const posts = data.allMarkdownRemark.nodes

  if (posts.length === 0) {
    return (
      <Layout location={location} title={siteTitle}>
        <Seo title="No Posts Found" />
        <p>No blog posts found.</p>
      </Layout>
    )
  }

  return (
    <Layout location={location} title={siteTitle}>
      <Seo title="Blog | Zacchary Puckeridge" />
      <ol style={{ listStyle: `none` }}>
        {posts.map(post => {
          const title = post.frontmatter.title || post.fields.slug
          const tags = post.frontmatter.tags

          return (
            <li key={post.fields.slug}>
              <article
                className="post-list-item"
                itemScope
                itemType="http://schema.org/Article"
              >
                <header>
                  <h2>
                    <Link to={post.fields.slug} itemProp="url">
                      <span itemProp="headline">{title}</span>
                    </Link>
                  </h2>
                  <small>{post.frontmatter.date} ◦ </small>
                  {tags && tags.length > 0 ? `` : ` ◦ `}
                  <Tags>{tags}</Tags><small> ◦ </small>
                  <small>{post.fields.readingTime}</small>
                </header>
                <section>
                  <p>{post.frontmatter.description}</p>
                </section>
              </article>
            </li>
          )
        })}
      </ol>
    </Layout>
  )
}

export default BlogIndex

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      nodes {
        fields {
          slug
        }
        frontmatter {
          date(formatString: "MMMM DD, YYYY")
          title
          description
          tags
        }
      }
    }
  }
`

Blog Post Template

import * as React from "react"
import { Link, graphql } from "gatsby"

import Layout from "../components/layout"
import Seo from "../components/seo"
import Comments from "../components/comments"
import Tags from "../components/tags"

const BlogPostTemplate = ({ data, location }) => {
  const post = data.markdownRemark
  const siteTitle = data.site.siteMetadata?.title || `Title`
  const { previous, next } = data
  const tags = post.frontmatter.tags

  return (
    <Layout location={location} title={siteTitle}>
      <Seo
        title={post.frontmatter.title}
        description={post.frontmatter.description}
      />
      <article
        className="blog-post"
        itemScope
        itemType="http://schema.org/Article"
      >
        <header>
          <h1 itemProp="headline">{post.frontmatter.title}</h1>
          <small>{post.frontmatter.date} ◦ </small>
          <Tags>{tags}</Tags>
          <small> ◦ </small>
          <small>{post.frontmatter.readingTime}</small>
        </header>
        <section
          dangerouslySetInnerHTML={{ __html: post.html }}
          itemProp="articleBody"
        />
        <hr />
      </article>
      <nav className="blog-post-nav desktop-only">
        <ul className="article-navigation-container">
          <li className="article-navigation">
          <div className="navigation-title-prev">← Previous Article</div>
            {previous && (
              <Link className="navigation-link-prev" to={previous.fields.slug} rel="prev">
                {previous.frontmatter.title}
              </Link>
            )}

          </li>
          <li className="article-navigation">
          <div className="navigation-title-next">Next Article →</div>
            {next && (
              <Link className="navigation-link-next" to={next.fields.slug} rel="next">
                {next.frontmatter.title}
              </Link>
            )}
          </li>
        </ul>
      </nav>
      <Comments />
    </Layout>
  )
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query BlogPostBySlug(
    $id: String!
    $previousPostId: String
    $nextPostId: String
  ) {
    site {
      siteMetadata {
        title
      }
    }
    markdownRemark(id: { eq: $id }) {
      id
      html
      frontmatter {
        title
        date(formatString: "MMMM DD, YYYY")
        description
        tags
        readingTime
      }
    }
    previous: markdownRemark(id: { eq: $previousPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
      }
    }
    next: markdownRemark(id: { eq: $nextPostId }) {
      fields {
        slug
      }
      frontmatter {
        title
      }
    }
  }
`

Test it out!

Now that we’ve got all the pieces we need to have a fully functioning tag system in place, we need to test it all works! Throw your blog into development mode and take a test drive. If you run into any issues, you may need to tweak the styling to match your site theme/configuration.

Conclusion

Tags are incredibly important to any good blogging site. The Gatsby documentation can be a little confusing in it’s approach to implement them, so I hope I’ve helped a little to provide an alternative configuration!

If you have any questions or think I could have taken a better approach, let me know! Feel free to reach out in the comments below or reach out to me via email.

← Previous Article

Next Article →

Made with Gatsby, React, Chakra UI and ❤️