Front End Programming

Gatsbyブログサイト移行物語~プラグインを利用して目次出力~

ReactGatsby

記事に目次をつけたかったのでプラグインgatsby-remark-autolink-headersを利用して目次を実装しました。

ulタグからolタグに変え、目次が長くなるので閉じるボタンをつけ、アコーディオンさせるなど少し改造しました。そのやり方について綴ります。

※ 2021年12月v4バージョンアップに伴いリライトしました。

今までのGatsbyの記事と注意点

現在ここまで記載しています。
制作するまでを目標にUPしていくので順を追ったらGatsbyサイトが作れると思います。

  1. インストールからNetlifyデプロイまで
  2. ヘッダーとフッターを追加する
  3. 投稿テンプレにカテゴリやらメインビジュアル(アイキャッチ)追加
  4. ブログ記事、カテゴリ、タグ一覧の出力
  5. プラグインを利用して目次出力(←イマココ)
  6. プラグインナシで一覧にページネーション実装
  7. 個別ページテンプレート作成
  8. プラグインHelmetでSEO調整
  9. CSSコンポーネントでオリジナルページを作ろう!!
  10. 関連記事一覧出力
  11. タグクラウドコンポーネントを作成する
  12. パンくずリストを追加する
  13. 記事内で独自タグ(コンポーネント)を使えるようにする
※ Gatsbyは2021月12月、v4にバージョンアップしています。随時リライトしています。

このシリーズはGithub・gatsby-blogに各内容ブランチごとで分けて格納しています。

今回のソースはtable-of-contentブランチにあります。

このシリーズではテーマGatsby Starter Blogを改造

この記事は一番メジャーなテンプレート、「Gatsby Starter Blog」を改造しています。同じテーマでないと動かない可能性があります。

GatsbyJSは豊富なプラグインが魅力です。

gatsby-remark-autolink-headers はプラグインの1つです。
以下のようなことができます。

  • 見出しタグにidを振る
  • 見出しタグを抽出しリンク付きのリストタグを出力

gatsby-remark-autolink-headers

npmインストールします。

コマンド
npm install gatsby-remark-autolink-headers

gatsby-config.jsにプラグインの追記

gatsby-remark-autolink-headersgatsby-transformer-remark のサブプラグインです。
なので、gatsby-config.jsの gatsby-transformer-remarkのoption に記載します。

テーマGatsby Starter Blogを利用していればgatsby-transformer-remarkはインストールされているはずです。

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [`gatsby-remark-autolink-headers`],      },
    },
  ],
}



このプラグイン1コ問題があって、公式サイトによるとプラグインgatsby-remark-prismjsよりも前に読み込む必要があります。

// good
{
  resolve: `gatsby-transformer-remark`,
  options: {
    plugins: [
      `gatsby-remark-autolink-headers`,
      `gatsby-remark-prismjs`,
    ],
  },
}

// bad
{
  resolve: `gatsby-transformer-remark`,
  options: {
    plugins: [
      `gatsby-remark-prismjs`, // should be placed after `gatsby-remark-autolink-headers`
      `gatsby-remark-autolink-headers`,
    ],
  },
}

実装します。

オプションの説明については記事の後ろに記載します。

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [          {            resolve: `gatsby-remark-autolink-headers`,            options: {              icon: false,              maintainCase: false,            },          },        ],      },
    }
  ]
}

今回はシンプルにアイコンなし。以下のように設定しました。

entry410 2

オプションを設定しなければ、以下のように無駄なコードも出力されます。

entry410 1

optionのmaintainCaseをtrueにするとアンカーリングが効かなくなることも

maintainCaseをtrueにすると見出しに追加されるidに含まれるアルファベットは大文字が小文字に変換されます。目次のアンカーと差異ができて飛ばなくなることがあります。なのでここではfalseにしておきましょう。

目次を出力するコンポーネントを作成する

次に目次を出力するコンポーネントを作成します。

table-of-content.jsを追加します。

src/
    ├ templates/
    |   └ blog-post.js
    └ components/
        └ table-of-content.js(新規作成)

コードはこんな感じです。

table-of-content.js
import React from "react";

const Topic = props => {
  return (
    <div>
      <h2>目次</h2>
        <div
          dangerouslySetInnerHTML={{
            __html: props.data,
          }}
        >
        </div>
      </div>
  );
};

export default Topic;

リスト化されたデータはdata.markdownRemark.tableOfContentsに格納されます。

blog-post.jsのGraghQLのスキーマmarkdownRemark()内にtableOfContentsを追記します。


ポイントはmaxDepthを指定することによって表示する見出しの深さを調整できます。

tableOfContents(maxDepth: 3)
かみーゆ
かみーゆ

目次のネスト(入れ子)が深いのはあまり好きじゃないんだよね

ということで、見出し3(maxDepth: 3)まで取得することにしました。

あとは記事詳細テンプレの読み込みたい場所にコンポーネントを出力するだけです。

blog-post.js
import TOC from "../components/table-of-content"
//~コード省略~
const BlogPostTemplate = ({ data, location }) => {
  {/*~コード省略*/}
  <TOC data={data.markdownRemark.tableOfContents} />  {/*~コード省略*/}
}

export default BlogPostTemplate

export const pageQuery = graphql`
  query BlogPostBySlug(
    $id: String!
    $previousPostId: String
    $nextPostId: String
    $hero: String
  ) {
    site {
      siteMetadata {
        title
      }
    }
    allFile(
      filter: {
        relativePath: { eq: $hero }
        sourceInstanceName: { eq: "images" }
      }
    ) {
      edges {
        node {
          relativePath
          childImageSharp {
            gatsbyImageData(
              width: 640
              formats: [AUTO, WEBP, AVIF]
              placeholder: BLURRED
            )
          }
        }
      }
    }
    markdownRemark(id: { eq: $id }) {
      id
      excerpt(pruneLength: 160)
      html
      tableOfContents(maxDepth: 3)      frontmatter {
        title
        date(formatString: "YYYY-MM-DD")
        description
        cate
        tags
      }
    }
    # 省略
  }
`

出力されるタグをulからolに変え、開閉ボタンをつける

リスト出力がul(アンオーダーリスト・順不同リスト)なのは個人的にはちょっと気に入らないです。
なのでここから少し改変します。

JavaScript replaceul> から ol> に置換します。
(閉じタグもあるのでこのような形にしました)

後ほどアコーディオン機能を実装します。チェックボックスを追加して h2label に書き換えておきます。

table-of-content.js
import React from "react";

const Topic = props => {
  const list = props.data.replace(/(ul>)/gi, 'ol>');

  return (
    <div className="p-box--gray u-mblg">
      <input type="checkbox" class="mokuji" id="mokuji" />      <label className="c-content__heading" for="mokuji">目次</label>      <div className="c-editArea mokujiList">
        <div
          dangerouslySetInnerHTML={{
            __html: list,
          }}
        >
        </div>
      </div>
    </div>
  );
};

export default Topic;

スタイリングする

カウンター関数を利用してスタイリング、アコーディオンは手間なのでCSSのみで実装しました。

今回はコードしか紹介しませんので詳しく原理を知りたい方はこちらをご覧ください。

table-of-content.js
import React from "react";
import styled from "styled-components" //追加
const TableOfContent = props => {
  const list = props.data.replace(/(ul>)/gi, "ol>")

  return (
    <TOC>      <input type="checkbox" class="mokuji" id="mokuji" />
      <label className="heading" for="mokuji">
        目次
      </label>
      <div
        dangerouslySetInnerHTML={{
          __html: list,
        }}
      ></div>
    </TOC>  )
}
export default TableOfContent
// 省略

style用のコンポーネントのコードです。

table-of-content.js
const TOC = styled.div`
  border: 1px solid #aaa;
  padding: 0;
  margin: 20px 0;

  input {
    display: none;

    &:checked ~ div {
      max-height: 0;
    }
    &:checked ~ .heading::before {
        transform: rotate(90deg);
    }
  }
  div {
    transition: .3s;
    max-height: 200vh;
    overflow: hidden;
    p {
      margin: 0;
    }

    ol {
      counter-reset: cnt;
      list-style: none;
    }
    & > ol {
      margin: 0;
      padding: 10px 20px;
      border-top: 1px solid #aaa;

      li {
        counter-increment: cnt;
        position: relative;
        padding-left: 2em;
        &::before {
          left: 0;
          font-size: 1.4rem;
          font-weight: 700;
          position: absolute;
          content: counters(cnt, " - ")'.' ;
        }
        ol {
          padding-left: 0;

          li {
            padding-left: 3em;
          }
        }
      }
    }
  }
  .heading {
    background: #eee;
    font-size: 1.4rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    padding: 0 20px;
    height: 40px;
    font-size: 18px;
    margin: 0;
    position: relative;

    &::after,
    &::before {
      position: absolute;
      content: '';
      height: 2px;
      width: 20px;
      background: #999;
      right: 20px;
      top: 19px;
      transition: .3s;
    }
  }
`

コンポーネント化してスタイルを書きたい場合は、styled-componentsのインストールが必要です。

コマンド
npm i styled-components
完成



最初から閉じておきたい場合は、inputcheckedを付与しておきましょう。

<input type="checkbox" class="mokuji" id="mokuji" checked>

オプションの一覧

オプションの一覧です。icon以外はあまり使うことないかもしれません。

オプション用途
offsetYリンクをクリックして移動した時の見出しの上の空き(オフセット)の調整。pxです
iconBoolean。デフォルトはtrueでホバーすると左にアイコンが表示されまます。
classアンカーに独自のクラス名を指定するそう
maintainCaseBoolean。含まれる要は大文字小文字を維持するか指定できる。
removeAccentsBoolean。アクセント削除。日本人のサイトにはまず必要なさそう。
isIconAfterHeaderBoolean。アイコンの位置を右側に移動
elementsリンクを自動挿入するためのタグ一覧を配列で指定

まとめ

目次があると記事の全貌がわかり、読むか読まないかの判断ができ、ユーザーに優しいです。

次の記事は「プラグインナシで一覧にページネーション実装」です。

実際私のサイトでもヒートマップで確認すると、目次ってかなりクリックされているんですよ。

この記事が皆さんのコーディングライフの一助となれば幸いです。

最後までお読みいただきありがとうございました。

お読みいただきありがとうございます。
「銀ねこアトリエ」をより良いブログにするために是非応援してください!

銀ねこアトリエを応援する