Gatsby にタグ機能を追加する

https://www.gatsbyjs.com/starters/gatsbyjs/gatsby-starter-blog/ こちらをスターターとして使いましたが、タグの機能がありません。
記事にタグをつけて、タグリストのページを作ります。
(この記事は公式の日本語訳です)

post に tags を追加する

配列でタグを追加します。

---
title: 'Gatsby にタグ機能を追加する'
published: '2020-09-19'

draft: false

tags: ['Gatsby', 'タグ', 'tag']
---

...

サーバを再起動して、フィールドを追加できるようにします。

クエリを追加して、タグを取得します

GraphiQL で、以下のクエリが通ることを確認します。

GraphiQL
{
  allMarkdownRemark {
    group(field: frontmatter___tags) {
      tag: fieldValue
      totalCount
    }
  }
}

タグページテンプレートを作成します(/tags/{tag})

タグページテンプレートを作成して、そのあと gatsby-node.js 内で createPages して使います。

src/templates/tags.js
import React from "react"

// Components
import { Link, graphql } from "gatsby"
const Tags = ({ pageContext, data }) => {
  const { tag } = pageContext
  const { edges, totalCount } = data.allMarkdownRemark
  const tagHeader = `${totalCount} post${
    totalCount === 1 ? "" : "s"
  } tagged with "${tag}"`
  return (
    <div>
      <h1>{tagHeader}</h1>
      <ul>
        {edges.map(({ node }) => {
          const { slug } = node.fields
          const { title } = node.frontmatter
          return (
            <li key={slug}>
              <Link to={slug}>{title}</Link>
            </li>
          )
        })}
      </ul>
      {/*
        This links to a page that does not yet exist.
        You'll come back to it!
      */}
      <Link to="/tags">All tags</Link>
    </div>
  )
}

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
          }
        }
      }
    }
  }
`

gatsby-node.js でテンプレートを使ったレンダリングを追加する

gatsby-node.js
const path = require('path')
const _ = require('lodash')

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

  const blogPostTemplate = path.resolve('src/templates/blog.js')
  const tagTemplate = path.resolve('src/templates/tags.js')

  const result = await graphql(`
    {
      postsRemark: allMarkdownRemark(
        sort: { order: DESC, fields: [frontmatter___date] }
        limit: 2000
      ) {
        edges {
          node {
            fields {
              slug
            }
            frontmatter {
              tags
            }
          }
        }
      }
      tagsGroup: allMarkdownRemark(limit: 2000) {
        group(field: frontmatter___tags) {
          fieldValue
        }
      }
    }
  `)

  // handle errors
  if (result.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }

  const posts = result.data.postsRemark.edges

  // Create post detail pages
  posts.forEach(({ node }) => {
    createPage({
      path: node.fields.slug,
      component: blogPostTemplate,
    })
  })

  // 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,
      },
    })
  })
}

全ての記事にある全タグをリストアップするページを作る

src/pages/tags.js
import React from "react"

// Utilities
import kebabCase from "lodash/kebabCase"

// Components
import { Helmet } from "react-helmet"
import { Link, graphql } from "gatsby"

const TagsPage = ({
  data: {
    allMarkdownRemark: { group },
    site: {
      siteMetadata: { title },
    },
  },
}) => (
  <div>
    <Helmet title={title} />
    <div>
      <h1>Tags</h1>
      <ul>
        {group.map(tag => (
          <li key={tag.fieldValue}>
            <Link to={`/tags/${kebabCase(tag.fieldValue)}/`}>
              {tag.fieldValue} ({tag.totalCount})
            </Link>
          </li>
        ))}
      </ul>
    </div>
  </div>
)

export default TagsPage

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

これでタグを取得したページができました。タグ一覧からどれかクリックして、タグに該当する記事が選択されていれば出来上がり。

記事にタグをつける

せっかくなので、個別の記事にタグをつけてみましょう。
といっても、クエリに tags を追加して、呼び出すだけです。

例)

src\templates\blog-post.js
...
import Tag from '../components/tag'
...
        <header>
          <div className="postdate">{post.frontmatter.date}</div>
          <h1 itemProp="headline" className="posttitle">
            {post.frontmatter.title}
          </h1>
          <Tag tags={post.frontmatter.tags} /> ←ココ
        </header>

...
export const pageQuery = graphql`
  query BlogPostBySlug($slug: String!) {
    site {
      siteMetadata {
        title
      }
    }
    markdownRemark(fields: { slug: { eq: $slug } }) {
      id
      excerpt(pruneLength: 160)
      html
      tableOfContents
      frontmatter {
        title
        date(formatString: "YYYY/MM/DD")
        description
        tags ←ココ
      }
    }
  }
`
...
src\components\tag.js
import React from 'react'
import { Link } from 'gatsby'
import _ from 'lodash'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faTag } from '@fortawesome/free-solid-svg-icons'

const Tag = props => {
  library.add(faTag)

  return (
    <div className={props.index ? 'tag-index' : 'tag'}>
      {props.tags.map((tag, index) => {
        return (
          <Link
            to={`/tags/${_.kebabCase(tag)}/`}
            key={index}
            className={props.index ? 'tag__listindex' : 'tag__list'}
          >
            <FontAwesomeIcon icon={faTag} />
            {tag}
          </Link>
        )
      })}
    </div>
  )
}

export default Tag

tag.js は、index ページと各ポストで共有しています。
ソースが長くなると見通し悪くなってなんだか辛いので。

次回は tag.js で使用した FontAwesome について。

参考

https://www.gatsbyjs.com/docs/adding-tags-and-categories-to-blog-posts/#reach-skip-nav