はじめに
今回やること
Gatsby.jsで作成中のブログに、内容を表すタグ(カテゴリ)を設定できるようにします。 ※タグとカテゴリは、厳密には意味が異なるようなのでここではタグに統一します。
この記事では、タグごとに「そのタグがついている記事一覧」を表示できるようにします。
機能の前提
- 1つの記事に対し、複数のタグをつけることを可能とします。
- タグは記事内の
frontmatter
で配列として設定することにします。 - タグは日本語も可能としますが、「そのタグがついている記事一覧」のURLは半角英数字とします。
- 「そのタグがついている記事一覧」のURLは、タグの内容と1:1で紐づけ、JSONで管理します。
- タグ自体の一覧を表示できるようにします。
(余談)背景
このブログでは、元々記事をカテゴリ管理しようと「ナビゲーションバー」を用意していましたが、難しそうなので未実装でした。
そのままGoogleAdSenseの審査に出したところ、以下の理由で不合格となりました。。
サイトの仕様: ナビゲーション
プログラム ポリシーに記載されているとおり、Google 広告を表示するサイトやアプリでは、ユーザーに役立つ情報を提供することになっております。ユーザーが期待どおりの製品、商品、サービスを見つけられるように、サイトやアプリ内を簡単に移動できる必要があります。誤解を招く操作性とは、たとえば次のようなものを指します。
存在しないダウンロード/ストリーミング配信コンテンツの提供を謳っている 存在しないコンテンツにリンクしている 無関係なページや誤解を招くページもユーザーをリダイレクトしている サイトのテーマやビジネスモデルと関係がないテキストが記載されている 詳しくは、ウェブマスター向け品質に関するガイドラインと AdSense のプログラム ポリシーをご参照ください。
ということなので、ナビゲーションバーをクリックしたときに表示できるページを作成すべく、慌ててタグ一覧機能を実装してみた次第です。
公式チュートリアル
基本的には公式チュートリアルに沿って進めます。
全体の流れ
- 各記事にタグを設定する。
- 『タグごとの、そのタグがついている記事一覧』ページのテンプレートとなるコンポーネントを作成する。
gatsby-node.js
ファイルに、『タグごとの、そのタグがついている記事一覧』ページを作成する関数を記載する。この関数が実行されるのはビルド時。関数の中身は以下。- GraphQLで、全ての記事がもつタグを一覧で取得する。
- 存在するタグの数だけ、ページを作成する。指定されたテンプレートと、指定されたURLを利用する。
- ページを作成するときに必要な値を、テンプレートのコンポーネントに渡す。
各記事にタグを設定する
MDXファイルのfrontmatter
内にタグを設定します。
---
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
では記事の中身(タイトルなど)を取得しています。
コンポーネントの中身作成
コンポーネントの中身としては以下のようになります。
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は別管理としている部分です。(日本語をタグに使いたいため)
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
に以下ファイルを作成します。
const AllTagDefinition = [
{
name: "プログラミング",
slug: "programming",
},
{
name: "Linux",
slug: "linux",
},
{
name: "React",
slug: "react",
},
・・・省略(すべてのタグに対して設定)・・・
]
// オブジェクトにしてエクスポート
module.exports = { AllTagDefinition }
これをインポートし、タグ名と一致するレコードを取得します。
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
の判定を入れています。
最後に、
- 取得した
tagSlug
をpath
に指定 - 作成したテンプレート(
tags.js
)をcomponent
に指定 - テンプレートに引き渡すための
context
にtag
の値を指定
して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のブログに対し、タグを設定し、タグごとに記事一覧ページを作成することができました。