Gatsbyブログの記事をタグ(カテゴリ)管理する:その① 記事一覧ページを作る

2022/4/18

(最終更新: 2022/4/18

はじめに

今回やること

Gatsby.jsで作成中のブログに、内容を表すタグ(カテゴリ)を設定できるようにします。 ※タグとカテゴリは、厳密には意味が異なるようなのでここではタグに統一します。

この記事では、タグごとに「そのタグがついている記事一覧」を表示できるようにします。

タグの記事一覧ページイメージ

参考資料
以下のようなタグ一覧ページについては、こちらの記事で説明します。
Gatsbyブログの記事をタグ(カテゴリ)管理する:その②タグ一覧ページを作る

Gatsbyブログの記事をタグ(カテゴリ)管理する:その②タグ一覧ページを作る

Gatsby.jsで作成しているブログにタグ機能を実装します。今回はタグ一覧ページを作成します。 ハマったポイントと合わせて解説します。

https://bunsugi.com/gatsby-blog-tags-list

タグ一覧ページ

機能の前提

  • 1つの記事に対し、複数のタグをつけることを可能とします。
  • タグは記事内のfrontmatterで配列として設定することにします。
  • タグは日本語も可能としますが、「そのタグがついている記事一覧」のURLは半角英数字とします。
  • 「そのタグがついている記事一覧」のURLは、タグの内容と1:1で紐づけ、JSONで管理します。
  • タグ自体の一覧を表示できるようにします。

(余談)背景

このブログでは、元々記事をカテゴリ管理しようと「ナビゲーションバー」を用意していましたが、難しそうなので未実装でした。

そのままGoogleAdSenseの審査に出したところ、以下の理由で不合格となりました。。

サイトの仕様: ナビゲーション

プログラム ポリシーに記載されているとおり、Google 広告を表示するサイトやアプリでは、ユーザーに役立つ情報を提供することになっております。ユーザーが期待どおりの製品、商品、サービスを見つけられるように、サイトやアプリ内を簡単に移動できる必要があります。誤解を招く操作性とは、たとえば次のようなものを指します。

存在しないダウンロード/ストリーミング配信コンテンツの提供を謳っている 存在しないコンテンツにリンクしている 無関係なページや誤解を招くページもユーザーをリダイレクトしている サイトのテーマやビジネスモデルと関係がないテキストが記載されている 詳しくは、ウェブマスター向け品質に関するガイドラインと AdSense のプログラム ポリシーをご参照ください。

ということなので、ナビゲーションバーをクリックしたときに表示できるページを作成すべく、慌ててタグ一覧機能を実装してみた次第です。

公式チュートリアル

基本的には公式チュートリアルに沿って進めます。

参考資料

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

全体の流れ

  1. 各記事にタグを設定する。
  2. 『タグごとの、そのタグがついている記事一覧』ページのテンプレートとなるコンポーネントを作成する。
  3. gatsby-node.jsファイルに、『タグごとの、そのタグがついている記事一覧』ページを作成する関数を記載する。この関数が実行されるのはビルド時。関数の中身は以下。
    1. GraphQLで、全ての記事がもつタグを一覧で取得する。
    2. 存在するタグの数だけ、ページを作成する。指定されたテンプレートと、指定されたURLを利用する。
    3. ページを作成するときに必要な値を、テンプレートのコンポーネントに渡す。

各記事にタグを設定する

MDXファイルのfrontmatter内にタグを設定します。

index.mdx
---
title: 【MWAA】AWSのワークフロー管理システムMWAAをさわってみた。
slug: mwaa-beginner
created: 2022-04-03
modified-date:
    - 2022-04-07
tags:     - プログラミング    - AWS    - MWAA    - Airflowdescription: >
    AWSサービスのMWAA(Managed Workflows for Apache Airflow)を初めて使ってみます。MWAAは、Apatch AirFlowというワークフロー管理システムをAWSマネジメントサービスとして提供したものです。
---

『タグごとの、そのタグがついている記事一覧』ページのテンプレートとなるコンポーネントを作成する

ページを作成するときのテンプレートとなるコンポーネントを作成します。

src/templates/tags.jsを作成します。

GraphQLクエリの作成

GraphQLのクエリは以下のようにしています。

import { graphql } from "gatsby"

export const pageQuery = graphql`
  query ($tag: String) {
    site {
      siteMetadata {
        title
      }
    }
    allMdx(
      filter: { frontmatter: { tags: { in: [$tag] } } }
      sort: { fields: frontmatter___created, order: DESC }
    ) {
      totalCount
      edges {
        node {
          frontmatter {
            title
            modified_date(formatString: "YYYY/M/DD")
            thumbnail_image {
              childImageSharp {
                gatsbyImageData
              }
            }
            created(formatString: "YYYY/M/DD")
            tags
            description
            slug
          }
        }
      }
    }
  }
`

このテンプレートは、後で作るページ作成関数から【タグ1つ1つに対して】呼ばれます。

「タグ」の値はページ作成関数から引き渡すようにします。

query ($tag: String) {とあるとおり、クエリの引数がtagになっています。この値は引き渡されたものを使います。

allMdx(
    filter: { frontmatter: { tags: { in: [$tag] } } }
    sort: { fields: frontmatter___created, order: DESC }
)

の部分で、「引き渡されたタグをfrontmatter.tagsに含むMDXノード(記事)を抽出しています。

totalCount

で抽出した記事の数を取得できます。

edges

では記事の中身(タイトルなど)を取得しています。

コンポーネントの中身作成

コンポーネントの中身としては以下のようになります。

src/templates/tags.js
import React from "react"
import PropTypes from "prop-types"

import { graphql } from "gatsby"
import Seo from "../components/seo"
import PostCard from "../components/organisms/post-card"
import TagsList from "../components/organisms/tags-list"
import { Helmet } from "react-helmet"

const Tags = ({ pageContext, data, location }) => {  const { tag } = pageContext  const { edges, totalCount } = data.allMdx
  return (
    <Layout location={location}>
      <Seo title={`${tag}タグの記事一覧`} />
      <Helmet>
          <meta name="robots" content="noindex" />
      </Helmet>
      <h1>
        {`${tag}タグの記事一覧`}
      </h1>
      <div>
        {`${totalCount}`}
      </div>
      <ol style={{ listStyle: `none` }}>
        {edges.map((edge) => {
          const post = edge.node
          const title = post.frontmatter.title || "タイトルなし"
          const slug = post.frontmatter.slug
          const created = post.frontmatter.created
          const lastModified =
            post.frontmatter.modified_date[0] || post.frontmatter.created
          const description = post.frontmatter.description || post.excerpt

          return (
            <PostCard
              title={title}
              slug={slug}
              created={created}
              lastModified={lastModified}
              description={description}
            />
          )
        })}
      </ol>
    </Layout>
  )
}

※自作コンポーネントも入っていますがポイント部分とは関係ありません。

まず

const Tags = ({ pageContext, data, location }) => {
    const { tag } = pageContext

の部分からわかるように、ページ作成関数からpageContextというpropsを受け取ることが可能です。 pageContext.tagを分割代入で取り出しています。

const { edges, totalCount } = data.allMdx

ではGraphQLで取得した記事データを変数に分割代入しています。

{edges.map((edge) => {
          const post = edge.node
          const title = post.frontmatter.title || "タイトルなし"
          const slug = post.frontmatter.slug
          const created = post.frontmatter.created
          const lastModified =
            post.frontmatter.modified_date[0] || post.frontmatter.created
          const description = post.frontmatter.description || post.excerpt

          return (
            <PostCard
              title={title}
              slug={slug}
              created={created}
              lastModified={lastModified}
              description={description}
            />
          )
        })}

ここで、記事の一覧をmap関数を使って記事1つずつ描画します。 edgesにはすでにtagで絞り込んだ記事データが入っているので、難しいことはありません。

(参考)ブログの記事を作成する処理とのちがい

ブログの記事ページもMDXから自動生成しているので、基本的には同じようなものですが、タグ一覧ページのほうが少し複雑です。

ブログの記事ページを作成するためのテンプレートは、src/pages/{mdx.frontmatter__slug}.jsとしています。

これを作るだけで、gatsby-node.jsに何も書かなくても、ビルド時には内部的に 「MDXファイルから作成されたノード1つ1つに対し、frontmatter.slugをURLに使ってページを作成する」 という処理を行なうページ作成関数が動いてくれます。

一方でタグ一覧ページは 「MDXファイルから作成されたノード1つ1つに対し」 ではなく 「全記事を見たときに存在するタグ1つ1つに対し」 処理をする必要があります。

gatsby-node.jsファイルに、『タグごとの、そのタグがついている記事一覧』ページを作成する関数を記載する

前節で述べた通り、タグ1つ1つに対してページ作成をするためには、ビルド時に実行する関数を記載する必要があります。

全量は以下の通りです。

公式と違う部分は、公式ではタグ名をそのままタグページのURLとしているのに対し、私の場合はタグページのURLは別管理としている部分です。(日本語をタグに使いたいため)

gatsby-node.js
const path = require("path")
const _ = require("lodash")
const { AllTagDefinition } = require("./src/params/all-tag-difinition")

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

  // タグページのテンプレートを定義
  const tagTemplate = path.resolve(`./src/templates/tags.js`)

  // すべてのタグを取得する
  const result = await graphql(`
    {
      tagsGroup: allMdx {
        group(field: frontmatter___tags) {
          fieldValue
          totalCount
        }
      }
    }
  `)

  // エラー処理
  if (result.errors) {
    reporter.panicOnBuild(`Error while running GraphQL query.`)
    return
  }

  let tags = result.data.tagsGroup.group

  tags.forEach((tag) => {
    // タグの英名を取得する
    const tagDefinition = AllTagDefinition.filter(
      (item) => item.name === tag.fieldValue
    )
    const tagSlug =
      tagDefinition.length === 0 ? tag.fieldValue : tagDefinition[0].slug // 定義に無かった場合(filterした結果が空配列)は日本語のままslugにする。

    // createPage呼び出し
    createPage({
      path: `/tags/${_.kebabCase(tagSlug)}`,
      component: tagTemplate,
      context: {
        tag: tag.fieldValue,
      },
    })

  })
}

関数の基本構成

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

の部分は公式そのままです。

gatsbyでは{action}というpropsを受け、その中にあるcreatePageというページ作成関数を使うことができます。

createPage関数に渡す値を作り、これを呼び出すのが今作ろうとしているcreatePages関数のお仕事です。

テンプレート定義

テンプレートのコンポーネントを指定します。

const tagTemplate = path.resolve(`./src/templates/tags.js`)

GrahpQLでタグの一覧を取得する

GraphQLではgroupという取得の仕方ができます。

// すべてのタグを取得する
  const result = await graphql(`
    {
      tagsGroup: allMdx {
        group(field: frontmatter___tags) {
          fieldValue
          totalCount
        }
      }
    }
  `)

let tags = result.data.tagsGroup.group

SQLのGROUPのようなイメージで、frontmatter.tagsをキーにしてグルーピングします。

実際に取得する値はfieldValue(タグの値)と、タグごとのtotalCountです。

tagsGroup:部分で、allMdxの別名を指定しています。結果、result.data.tagsGroup.groupでタグ一覧の配列を取得することができます。

console.logしてみるとこのような形です。

[
  [Object: null prototype] { fieldValue: 'AWS', totalCount: 2 },
  [Object: null prototype] { fieldValue: 'Airflow', totalCount: 2 },
  [Object: null prototype] { fieldValue: 'Gatsbyブログ作成', totalCount: 6 },
  [Object: null prototype] { fieldValue: 'Linux', totalCount: 1 },
  [Object: null prototype] { fieldValue: 'MWAA', totalCount: 2 },
  [Object: null prototype] { fieldValue: 'Node.js', totalCount: 1 },
  [Object: null prototype] { fieldValue: 'React', totalCount: 2 },
  [Object: null prototype] { fieldValue: 'npm/yarn', totalCount: 1 },
  [Object: null prototype] { fieldValue: 'プログラミング', totalCount: 11 },
]

タグ1つ1つに対してcreatePage関数を呼び出す

タグ1つ1つに対し

  • タグページのURLにしたい値を取得する
  • createPageを呼び出す

を実行します。

まずタグページのURLにしたい値を定義します。

src/params/all-tag-difinition.jsに以下ファイルを作成します。

src/params/all-tag-difinition.js
const AllTagDefinition = [
  {
    name: "プログラミング",
    slug: "programming",
  },
  {
    name: "Linux",
    slug: "linux",
  },
  {
    name: "React",
    slug: "react",
  },
・・・省略(すべてのタグに対して設定)・・・
]

// オブジェクトにしてエクスポート
module.exports = { AllTagDefinition }

これをインポートし、タグ名と一致するレコードを取得します。

gatsby-node.js
const { AllTagDefinition } = require("./src/params/all-tag-difinition")
tags.forEach((tag) => {
// タグの英名を取得する
const tagDefinition = AllTagDefinition.filter(
    (item) => item.name === tag.fieldValue
)
const tagSlug =
    tagDefinition.length === 0 ? tag.fieldValue : tagDefinition[0].slug // 定義に無かった場合(filterした結果が空配列)は日本語のままslugにする。

tagSlugに抽出したレコードのslugを入れますが、AllTagDefinitionに定義し忘れた場合に備えてlength === 0の判定を入れています。

最後に、

  • 取得したtagSlugpathに指定
  • 作成したテンプレート(tags.js)をcomponentに指定
  • テンプレートに引き渡すためのcontexttagの値を指定

してcreatePageを呼び出します。

// createPage呼び出し
createPage({
    path: `/tags/${_.kebabCase(tagSlug)}`,
    component: tagTemplate,
    context: {
    tag: tag.fieldValue,
    },
})
})

はまったポイント

gatsby-node.jsの中ではいつものimportが使えません。 (ES6記法をサポートしていないため)

そのため AllTagDefinition側でmodule.exportsする必要があります。

このときAllTagDefinitionという名前のオブジェクトを作成し、

{ AllTagDefinition : AllTagDefinition(オブジェクト)}

というオブジェクトとしてexportしています。

gatsby-node.js側でrequireするときは

{ AllTagDefinition } = require("./src/params/all-tag-difinition")

とすることで、先ほどのオブジェクトから AllTagDefinitionを取り出すことができます。

{}を入れるのか入れないのか、require側とmodule.exports側で組み合わせを間違えると動きません。

結果

以下のように、各タグの記事一覧ページを作成することができました。

https://bunsugi.com/tags/mwaa

タグの記事一覧ページイメージ

まとめ

Gatsby.jsのブログに対し、タグを設定し、タグごとに記事一覧ページを作成することができました。



個別連絡はこちらへ→Twitterお問い合わせ

プロフィール

プロフィールイメージ

はち子

事業会社のシステム部門で働きはじめて5年目の会社員。システム企画/要件定義/システムアーキテクチャ等。

Twitter→@bun_sugi

過去の記事について

はてなブログに掲載の記事(主にプログラミングメモ)についてはこちらに掲載しております。(本ブログに移行中)

タグ一覧

関連記事

Copyright© 2023, エンジニアを目指す日常ブログ

お問い合わせ|プライバシーポリシー