Gatsby でパンくずリストを作る方法

JavaScriptJamstackSSGGatsby

Node.js ベースの SSG SSG のひとつ Gatsby でパンくずリストを実装する方法についてかんたんにまとめました。 私は Gatsby で実装しましたが、基本的な考え方は他の SSG でも同じと思いますので他の SSG を使う方にも参考になるかもしれません。

対象バージョン

動作確認は以下のバージョンで行いました。

  • Node.js: 16.x
  • Gatsby: v4 / v3

Gatsby でパンくずリストを作る方法

大きく 2 通りのアプローチがあります。

  • A) プラグイン gatsby-plugin-breadcrumbs を使う
  • B) 独自に実装する

A) プラグイン gatsby-plugin-breadcrumbs を使う

パンくずリスト作成用のプラグイン gatsby-plugin-breadcrumbs を使うアプローチです。

記事執筆時点で gatsby-plugin-breadcrumbs の最新バージョンは v12.3.1 です。 このバージョンでは次の 2 つのタイプのパンくずリストがサポートされています:

  • A1) クリック履歴: ユーザーのページクリック履歴(≒訪問履歴)からなるパンくずリスト
  • A2) URL パス: URL パスを / で分割して作られるパンくずリスト

プラグインの README では A1) は「 Click Tracking 」、 A2) は「 AutoGen 」と呼ばれています。 詳細に興味のある方はプラグインのリポジトリを参照してください。

A1) A2) どちらかのパターンのパンくずリストが欲しい場合はまず gatsby-plugin-breadcrumbs の利用を検討するとよいと思います。

B) 独自に実装する

イチから独自に実装する方法です。 次の 2 つのアプローチに大別できます。

  • B1) ハードコーディングする
  • B2) CMS やファイルで設定できるようにする

上の A1) A2) にあてはまらない場合はこちらを選ぶことになります。

B1) ハードコーディングする

B1) は Gatsby のページテンプレート( or コンポーネント)内でハードコーディングで実装するアプローチです。 以下の場合にこちらのアプローチが選択可能です。

  • サイトの構造がシンプルである
  • サイトの規模が小さい
  • 構築後の運用にプログラマが携われる(=サイトにその予算を割くだけの価値がある)

B2) CMS やファイルで設定できるようにする

他方の B2) は CMS やファイルなどを使ってパンくずリストを管理するアプローチです。 こちらは管理のための「データモデル」と「フォーマット」でさらに細かく分類できます。

「データモデル」の選択肢としてたとえば次のような形が考えられます:

  • M1) ページごとにパンくずリスト全体を管理する
  • M2) ページ間の親子関係を管理する

M1) は、たとえば次のように 1 ページずつ個別にパンくずリストを設定する形です。

ページ パンくずリスト
メンバー紹介 HOME / 会社概要 / メンバー紹介
WordPress サイト構築 HOME / サービス紹介 / WordPress サイト構築

M2) は、たとえば次のように各ページに対して「親ページ」を設定して、実際にパンくずリストを描画するときにはこの親子関係を終端(= HOME ページ)までたどっていくやり方です。

ページ 親ページ
メンバー紹介 会社概要
WordPress サイト構築 サービス紹介

「フォーマット」は具体的な保存形式のことです。 たとえば次の次のような選択肢があります。

  • データベーステーブル
  • CSV
  • JSON
  • Markdown の frontmatter

上の A1) A2) B1) のどのアプローチも使えない場合は、消去法的にこの B2) のアプローチを選ぶことになります。

M2) の「ページ間の親子関係を管理する」アプローチの具体的な実装イメージを以下で説明します。

実装イメージ: ページ間の親子関係を管理する

CMS 側

サイトのページの管理に WordPress などの CMS を使う場合は CMS を、使わない場合はその他の方法(ファイルなど)を使って、各ページに対して「親ページ」を設定できるようにします。

参照型のフィールドを扱える CMS であれば、ページ参照型の「親ページ」フィールドを各コンテンツタイプに追加します。

多くの場合、パンくずリストのラベルには各ページのタイトルを使えばよいですが、ページタイトルが使えない場合は「パンくずリストラベル」といったテキストフィールドを各コンテンツタイプに追加して対応します。

Gatsby 側

Gatsby 側では CMS 側で設定された親子関係を「親→その親→…」と順にたどってパンくずリストを生成します。

技術的に細かなお話になりますが、フィールドの resolve() を使うと比較的スムーズに実装できます。 次のコードは CustomPage というコンテンツタイプでパンくずリストを実装するサンプルです。

exports.createSchemaCustomization = ({ actions, schema }) => {
  const { createTypes } = actions
  const { buildObjectType } = schema

  const pageTypeDefs = [
    // CustomPage
    buildObjectType({
      name: `CustomPage`,
      fields: {
        title: `String!`,
        body: `String`,
        // 親ページ
        parent_page: {
          type: `CustomPage`,
          extensions: { link: {} },
        },
        // パンくず用フィールド
        // `parent_page` から動的に生成する
        breadcrumbs: {
          type: `[CustomPage!]!`,
          resolve: (source, args, context, info) => {
            // 親ページが無い場合は自身のみを含めて返す
            if (isNil(source.parent_page)) {
              return [source]
            }

            // 親ページがある場合は親ページのパンくずに自身を追加して返す
            const parentPage = context.nodeModel.getNodeById({
              // ここで `source.parent_page` は Object ではなく id 文字列
              id: source.parent_page,
              type: `CustomPage`,
            })

            const type = info.schema.getType(`CustomPage`)
            const resolver = type.getFields()[`breadcrumbs`].resolve
            const parentBreadcrumbs = resolver(parentPage, args, context, {
              // 親ページでも `info.schema` を利用したいので渡す
              ...info,
              fieldName: `breadcrumbs`,
            })

            return [...parentBreadcrumbs, source]
          },
        },
      },
      interfaces: [`Node`],
      extensions: { infer: false },
    }),
  ]

  createTypes(pageTypeDefs)
}

まず parent_page という参照型のフィールドを定義しています。 その上で breadcrumbs というフィールドを定義して、 parent_page の関係をたどってパンくずリストを作れるようにしています。 breadcrumbs フィールドでは resolve() を定義して source.parent_page を通して親ページを順に参照しています。 createNode() でノードを作成するときは parent_page フィールドの値だけセットすればよく、 breadcrumbs の値をセットする必要はありません。

尚、このコードだと、間違って親子関係がループするような設定になっている場合に無限ループが発生してしまいます。 実際のプロジェクトではこの無限ループを避けるために Gatsby ・ CMS ・運用のどこかで対応を行う必要があります。

このように breadcrumbs フィールドを用意した上で、各ページに対して titlepath の 2 つのフィールドを作成しておけば、あとは breadcrumbs の各要素に該当するノードをリンクにして並べれば OK です。

パンくずリストコンポーネントのイメージ:

breadcrumbs.js:

import * as React from "react"
import PropTypes from "prop-types"
import { Link } from "gatsby"

const Breadcrumbs = ({ items }) => {
  const length = items.length

  return (
    <nav>
      <Link to="/">ホーム</Link>
      <Separator />

      {items.map(({ title, href }, index) => {
        if (index < length - 1) {
          return (
            <React.Fragment key={`${href}___${title}`}>
              <Link to={href}>{title}</Link>
              <Separator />
            </React.Fragment>
          )
        } else {
          return (
            <span>{title}</span>
          )
        }
      })}
    </nav>
  )
}

Breadcrumbs.propTypes = {
  items: PropTypes.arrayOf(PropTypes.exact({
    title: PropTypes.string.isRequired,
    href: PropTypes.string,
  })).isRequired,
}

const Separator = () => {
  return (
    <span>/</span>
  )
}

export default Breadcrumbs

以上です。


  1. Static Site Generator 。静的サイトジェネレータ。

アバター
後藤隼人 ( ごとうはやと )

Python や PHP を使ってソフトウェア開発やウェブ制作をしています。詳しくはこちら