Gatsby×Contentful×リッチテキスト(Rich Text)で目次(Table of contents) | Gatsbyブログカスタマイズ

Webサイト高速化のための 静的サイトジェネレーター活用入門を通じて作成したこのブログのカスタマイズとして、記事ページに目次の表示を加えました。

目次表示の仕様

参考サイトのコードを参考に、以下の仕様としました。

  • 見出しはH2とH3のみとして目次も同様とする

  • 目次の項目をクリックすると対する見出しにページ内遷移する

  • ページ内遷移はスムーススクロールする

プラグイン(react-scroll)をインストール

スムーススクロールにはさまざまなプラグインがありますが、選択したのはReactのライブラリ、react-scrollです。
まずはインストールをしておきます。

yarn add react-scroll

npmの場合は以下の通り。

npm install react-scroll

tableofcontents.jsの作成

src/components/tableofcontents.jsを新規作成します。

後程blogpost-template.jsよりpropsでコンテンツのjsonを渡すので、それを受けて見出しのみを抽出します。
そして抽出した見出しをリストにして目次を生成します。

その際、見出しのテキストを加工してリンク先の指定としても利用します。
見出しのテキストを加工せずに利用すると日本語になってしまうので好ましいものではなく、半角英数への加工が必要でした。
半角英数への加工はmd5でのハッシュとすることで、以下を実現できました。
ちなみに、cryptoはNode.jsが備えるモジュールなので、別途インストールは必要ありません。

  • 半角英数である

  • 必ず32文字になる

  • 同じ文字列からは同じ結果となる

リンク先の指定はLinkでスムーススクロールとしています。

import React from "react"
import { BLOCKS } from '@contentful/rich-text-types'
import { documentToReactComponents } from "@contentful/rich-text-react-renderer"

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faListUl } from "@fortawesome/free-solid-svg-icons"

// スムーススクロール
import { Link } from "react-scroll"

// ハッシュ化用モジュールの読み込み
const crypto = require("crypto")

const TableOfContents = props => {

  // Contentfulの見出しを定義
  const headingTypes = [BLOCKS.HEADING_2, BLOCKS.HEADING_3]

  // 見出しを配列で格納
  const headings = props.json.content.filter(item => headingTypes.includes(item.nodeType))

  // 見出しのnodeTypeをdocumentとして利用
  const document = {
    nodeType: "document",
    content: headings,
  }

  const options = {
    renderNode: {
      [BLOCKS.HEADING_2]: (node, children) => {
        // H2タグのテキストをハッシュ化してidに利用
        const anchor = crypto.createHash("md5").update(node.content[0].value).digest("hex")
        return (
          <li>
            <Link
              to={anchor}
              activeClass="active"
              smooth={true}
              duration={500}
            >
              {children}
            </Link>
          </li>
        )
      },
      [BLOCKS.HEADING_3]: (node, children) => {
        // H3タグのテキストをハッシュ化してidに利用
        const anchor = crypto.createHash("md5").update(node.content[0].value).digest("hex")
        return (
          <li className="toc__child">
            <Link
              to={anchor}
              activeClass="active"
              smooth={true}
              duration={500}
            >
              {children}
            </Link>
          </li>
        )
      },
    }
  }

  return (
    <nav className="toc">
      <p className="toc__title"><FontAwesomeIcon icon={faListUl} />目次</p>
      <ul>
        {documentToReactComponents(document, options)}
      </ul>
    </nav>
  )
}

export default TableOfContents

blogpost-template.jsの編集

src/templates/blogpost-template.jsを編集します。

まず、上部で作成したTableOfContentsをimportします。

import TableOfContents from "../components/tableofcontents"

optionsでH2タグとH3タグに、見出しのテキストを加工してidとして指定します。

// Before
const options = {
  renderNode: {
    [BLOCKS.HEADING_2]: (node, children) => (
      <h2>
        <FontAwesomeIcon icon={faCheckSquare} />
        {children}
      </h2>
    ),
    ...

// After
// ハッシュ化用モジュールの読み込み
const crypto = require("crypto")

const options = {
  renderNode: {
    [BLOCKS.HEADING_2]: (node, children) => {
      // H2タグのテキストをハッシュ化してidに利用
      const anchor = crypto.createHash("md5").update(node.content[0].value).digest("hex")
      return (
        <h2 id={anchor}>
          <FontAwesomeIcon icon={faCheckSquare} />
          {children}
        </h2>
      )
    },
    [BLOCKS.HEADING_3]: (node, children) => {
      // H3タグのテキストをハッシュ化してidに利用
      const anchor = crypto.createHash("md5").update(node.content[0].value).digest("hex")
      return (
        <h3 id={anchor}>
          {children}
        </h3>
      )
    },
    ...

任意の箇所でTableOfContentsコンポーネントを使います。
併せて、tableofcontents.jsへコンテンツのjsonを渡します。

<TableOfContents json={data.contentfulBlogPost.content.json} />

スタイル調整

CSSでスタイルを調整します。
SCSSではありますが、参考までに以下に示します。

// 目次機能
.toc {
    width: 100%;
    background-color: #eee;
    padding: 1.5em;
    margin-top: 40px;
    box-sizing: border-box;

    &__title {
        margin-bottom: 1em;
    }

    ul {
        padding-left: 1.5em;

        li:not(:last-child) {
            margin-bottom: 0.5em;
        }
    }

    a {
        cursor: pointer;
    }

    &__child {
        margin-left: 1em;
    }
}

Gatsbyの3系に移行すると発生したエラー

Gatsbyが2系までだと問題なく使えていたcryptoが、3系に移行するとエラーを吐き出すようになってしまいました。

エラーログは以下の通り。

If you're trying to use a package make sure that 'crypto' is installed. If you're trying to use a local file make sure that the path is correct.

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "crypto": require.resolve("crypto-browserify") }'
        - install 'crypto-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "crypto": false }

このエラーに対応すると、新たなエラーが表示されます。

If you're trying to use a package make sure that 'stream' is installed. If you're trying to use a local file make sure that the path is correct.

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.

If you want to include a polyfill, you need to:
        - add a fallback 'resolve.fallback: { "stream": require.resolve("stream-browserify") }'
        - install 'stream-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
        resolve.fallback: { "stream": false }

webpackのバージョンが5になったため、互換性がなくなったようです。

Gatsbyの3系でcryptoを使用する

吐き出されたエラーに併せて対応します。

まず、crypto-browserifyとstream-browserifyをインストールします。

yarn add crypto-browserify stream-browserify

npmの場合はこちら。

npm install crypto-browserify stream-browserify

gatsby-node.jsに以下を追記します。

exports.onCreateWebpackConfig = ({ actions }) => {
  actions.setWebpackConfig({
    resolve: {
      fallback: {
        "crypto": require.resolve("crypto-browserify"),
        "stream": require.resolve("stream-browserify")
      },
    },
  })
}

参考サイト

▼How to create table of contents from Contentful's Rich Text field | Vince Parulan
https://vinceparulan.com/blog/how-to-create-table-of-contents-from-contentful-s-rich-text-field/

▼Adding a Custom webpack Config | Gatsby
https://www.gatsbyjs.com/docs/how-to/custom-configuration/add-custom-webpack-config/