初歩からの無職

Gatsby + Contentfulでタグ機能の導入

  • GatsbyJS
  • Contentful

Starterにタグによる記事分類がなかったので自分で実装します。コードは一部省略してますが、Contentful Gatsby Starter Blogをいじってる感じです。

Contentfulでタグ用のContent modelを作成する

ContentfulでTagsというContent modelを作ります。Contentfulのガイドに沿って作成したStarterではBlog postのfieldとしてtags fieldが生成されているのですが、タグをブログ記事などと同じようなcontent modelで定義しておくと後でいろいろ便利っぽいです。

contentful-tags-model

タグごとのページなども作成したいのでtitleとslugを設定。titleとslugに分けたのは日本語のタグにも英字URLを当てたい(心理学: psychologyなど)のでそうしましたが、後述するようにtitle=slugにしたほうが楽は楽でした。一応Hero Imageも設定できるようにしておきました。Refference fieldでタグの親子関係もできそうですが、階層型で整理するのも大変そうなのでとりあえず保留です。

tags modelを作ったので次はBlog postのfieldから参照できるようにします。

contentful-tags-model-reference

Add fieldからReferenceを選択

contentful-tags-new-reference-field

複数のタグを参照するので、Many referencesにチェックを入れてCreateします。

contentful-tags-field-validation

設定のValidationでTagsモデルを参照するようにします。

contentful-tags-field

これで各Blog postと各Tagを紐付けられるようになりました。

Gatsbyで各タグページを作成

Gatsbyで各タグページを作成します。StarterでのBlog postを参考に似たロジックで作ります。

Tagsで作ったtitleとslugをGraphQLで引っ張ります。

gatsby-node.js
allContentfulTags {
  edges {
    node {
      title
      slug
    }
  }
}

Blog postをcreatePageで各Tagページを作ります。contextでslugを渡しておきます。

gatsby-node.js
// tagページ作成のcomponent
const tagTemplate = path.resolve('./src/templates/tag-template.js')

// 中略
// resolve(graphql(`...`).then(result => {

const tags = result.data.allContentfulTags.edges;
tags.forEach(tag => {
  createPage({
    path: `tags/${tag.node.slug}/`,
    component: tagTemplate,
    context: {
      slug: tag.node.slug
    },
  })
})

これでtags/タグ名で各タグページが作成されました。

tag-page

タグ一覧ページを作成

/tags/でタグ一覧が表示されるようにします。

一覧ページではタグごとの記事数も表示したいと思います。Gatsbyはグループ化してfetchすることでtotalCountで総数を取得できるので、これを利用してタグごとの記事数を表示できます。Tagsのslugでfetchした場合はfieldValueにslugが入ることになります。前述の通りslugがそのままタグ名を表していれば楽なのですが、tagのtitleとslugを分けてしまったので、titleは別途突き合わせて表示させてあげる必要があるみたいです。

/pages/tags.js
query TagIndexQuery {
  allContentfulBlogPost {
    group(field: tags___slug) {
      fieldValue
      totalCount
    }
  }
  allContentfulTags {
    edges {
      node {
        slug
        title
        blog_post {
          slug
        }
      }
    }
  }
}

2021/1/11追記: 不要なクエリを削除しました

js部分は以下みたいな感じです。mapオブジェクトを連想配列に変換するスッキリする書き方に悩んだのですが、困ったときのstack overflowです。

タグ一覧はとりあえずで記事一覧を並べるコンポーネントを流用して並べました。

/pages/tags.js
import React from "react";
import { graphql, Link } from "gatsby";
import get from "lodash/get";
import Layout from "../components/layout";
import SEO from "../components/seo";

class BlogIndex extends React.Component {
  render() {
    // 各タグのtitleとslugの組み合わせ用
    const tags = get(this, "props.data.allContentfulTags.edges");
    // groupでfetchしたfieldValueとtotalCountのmapオブジェクトを取得
    const tagsGroup = get(this, "props.data.allContentfulBlogPost.group");
    // mapオブジェクトを連想配列に変換
    const tagsCount = Object.assign(...tagsGroup.map(({fieldValue, totalCount}) => ({[fieldValue]: totalCount})));

    return (
      <Layout location={this.props.location}>
        <SEO title="タグ一覧" desc="タグ一覧ページ" noindex />
        <div style={{ background: "#fff", textAlign: "center" }}>
          <div className="wrapper">
            <h2 className="section-headline">Tags</h2>
            <ul className="article-list">
              {tags.map(({ node }) => {
                return (
                  <li key={node.slug}>
                    <Link to={node.slug}>{node.title} ({tagsCount[node.slug]})</Link>
                  </li>
                );
              })}
            </ul>
          </div>
        </div>
      </Layout>
    );
  }
}

結果

tags-page

参考URL