GatsbyJSで作ったブログの記事にTagをつけて管理する

公開日: 2020年05月31日最終更新日: 2021年10月17日

ディレクトリを分けて記事を整理するようにしましたが、すべての記事を横断的に管理できるともっと良いです。 記事にタグを付けて管理できる方法をテーマがきっと用意してくれているはずだと思い調べました。
とは言いつつfrontmatterの記事tagsというのがあったので、それだと気づいてはいました。

(Creating Tags Pages for Blog Posts)[https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/]にタグ追加の方法が詳しく書かれています。 ここを参考にタグ一覧のページを追加します。
markdwonの記載は以下のように、frontmatterのtagsにリストとして記載する方法で合っていました。

---
title: 記事にTagをつけて管理する
date: 2020-05-31
slug: 2020-05-31-tag
tags: ["blog-log", "GatsbyJS", "tag"]
---

ただし、tagsを記載してもタグを表示するページがないため見ることができません。 そのため、タグ一覧のページとタグ別に記事の一覧を表示するページを作ります。 具体的には以下のファイルを作成しました。

  • src/pages/tags.js -> タグ一覧ページ
  • src/templates/tag-detail.js -> タグ別記事一覧ページのテンプレート
  • src/gatsby-node.js -> タグ一覧ページとタグ別記事一覧ページの生成処理を追加。

src/pagesディレクトリに配置したtags.jsはGatsbyJSによってそのままページとして構築されます。 タグ別記事一覧のページは、gatsby-node.jsにおいてタグ情報のクエリ結果をもとにtag-detail.jsをテンプレートとしてcreatePageを呼び出すことで生成します。

それでは、ページを作るために必要な情報を取得するGraphQLを作成し、それによって得られた結果を表示するJSXを記述していきます。
gatsby developを実行した状態で、"http://localhost:8000/___graphql"にアクセスするとGraphQLを試しながら構築できるツール(GraphiQL)が開きます。GraphiQLを使いながらGatsbyJSのドキュメントを参考に自分の環境にあったGraphQLにしていきます。
queryとデータ構造が一致しているため、実際にデータ構造を見ながら必要な情報を選択していけばよいので直感的に作ることができました。

内容は以下のとおりです。出来上がったページはこちらです

src/pages/tags.js
import React from "react"
import PropTypes from "prop-types"
import { Styled } from "theme-ui"

// Utilities
import kebabCase from "lodash/kebabCase"

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

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

TagsPage.propTypes = {
    data: PropTypes.shape({
        allMdxBlogPost: PropTypes.shape({
            group: PropTypes.arrayOf(
                PropTypes.shape({
                    fieldValue: PropTypes.string.isRequired,
                    totalCount: PropTypes.number.isRequired,
                }).isRequired
            ),
        }),
        site: PropTypes.shape({
            siteMetadata: PropTypes.shape({
                title: PropTypes.string.isRequired,
            }),
        }),
    }),
}

export default TagsPage

export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    allMdxBlogPost(limit: 2000) {
      group(field: tags) {
        fieldValue
        totalCount
      }
    }
  }
`
src/templates/tag-detail.js
import React from "react"
import PropTypes from "prop-types"
import { Styled } from "theme-ui"

// Components
import { Link, graphql } from "gatsby"

const TagDetail = ({ pageContext, data }) => {
    const { tag } = pageContext
    const { edges, totalCount } = data.allMdxBlogPost
    const tagHeader = `${totalCount} post${
        totalCount === 1 ? "" : "s"
        } tagged with "${tag}"`

    return (
        <div>
            <Styled.h1>{tagHeader}</Styled.h1>
            <Styled.ul>
                {edges.map(({ node }) => {
                    return (
                        <Styled.li key={node.slug}>
                            <Link to={node.slug}>
                                <Styled.a>
                                    {node.title}
                                </Styled.a>
                            </Link>
                        </Styled.li>
                    )
                })}
            </Styled.ul>
            {/*
              This links to a page that does not yet exist.
              You'll come back to it!
            */}
            <Link to="/tags">
                <Styled.a>
                    All tags
                </Styled.a>    
            </Link>
        </div>
    )
}

TagDetail.propTypes = {
    pageContext: PropTypes.shape({
        tag: PropTypes.string.isRequired,
    }),
    data: PropTypes.shape({
        allMdxBlogPost: PropTypes.shape({
            totalCount: PropTypes.number.isRequired,
            edges: PropTypes.arrayOf(
                PropTypes.shape({
                    node: PropTypes.shape({
                        title: PropTypes.string.isRequired,
                        slug: PropTypes.string.isRequired,
                    }),
                }).isRequired
            ),
        }),
    }),
}

export default TagDetail

export const pageQuery = graphql`
    query ($tag: String) {
        allMdxBlogPost(limit: 2000,
            sort: {fields: [date]},
            filter: {tags: {in: [$tag]}}) {
            totalCount
            edges {
                node {
                    title
                    slug
                }
            }
        }
    }  
`
src/gatsby-node.js
const path = require("path")
const _ = require("lodash")

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

    const tagTemplate = path.resolve("src/templates/tag-detail.js")

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

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

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

pageQueryがどこから実行されているのかまだ理解できていませんが、同じファイル内にしたpageQueryが実行されています。後で調べることにします。 そして、その結果がTagsPage.PropTypes / TagDetail.PropTypesに従った形でそれぞれのコンポーネントの引数に渡されています。 引数として受け取ってしまえばあとは好きなように表示するだけです。

しかし、GraphQLについてもっと調べないといけませんね……。

ちなみに、タグ一覧の表示についてはプラグインもあります。 https://www.gatsbyjs.org/packages/gatsby-theme-blog-tags/