Ryota Kondo

Ryota Kondo

2023/02/16

microCMS|繰り返しフィールドの要素から入れ子の目次を生成する方法

今回はmicroCMSの繰り返しフィールドを使用している場合に、その中で繰り返されるリッチエディタの要素から見出しを抽出し、目次を自動生成する方法について説明します。

目次のイメージ

生成する目次のイメージは下の通りです。見出し2、見出し3を抽出したものを入れ子構造のリストで表示するようにしています。画像ではわかりませんが、クリックすると対応する見出しへジャンプするリンクも付けています。

table of contents

それでは詳細の説明をいたします。

microCMSの設定

まずはmicroCMSの設定についてです。こちらには目次の表示/非表示をmicroCMS側で切り替えられるよう、設定項目を追加します。切り替えをしないのであれば設定は不要です。

コンテンツ(API)の設定

API設定の中からAPIスキーマを選択し、真偽値を追加します。

api schema

以上でmicroCMSの設定は完了です。

サーバサイドの実装

例としてフロントエンドフレームワークにNext.jsを使った場合で説明します。

typeの実装

microCMSから取得したコンテンツを格納するtypeは下の通りです。Contentは繰り返しフィールド、Blogは取得したコンテンツ本体となります。

// 繰り返しフィールド
export type Content = {
  fieldId: "rich_editor";
  rich_text: string;  // リッチエディタのテキスト
};

// コンテンツ本体
export type Blog = {
  id: string;
  createdAt: string;
  updatedAt: string;
  publishedAt: string;
  revisedAt: string;
  is_hide_toc: boolean;  // 目次の表示/非表示
  article_body: Content[];  // 繰り返しフィールドの配列
};

また、抽出した見出しを格納しておくtypeを下のように作成します。見出し2の子要素として見出し3を配列で保持する形となります。

// 見出し3
export type TocH3 = {
  id: string;
  text: string;  // 見出しテキスト
};

// 見出し2
export type TocH2 = {
  id: string;
  text: string;  // 見出しテキスト
  h3: TocH3[];  // 見出し3の配列
};

cheerioの導入

cheerio というライブラリを使って見出し要素を抽出します。使用するために、まずはインストールを行います。

npm install cheerio

次に目次を表示するページファイルにインポートします。

import { load } from "cheerio";

getStaticPropsの実装

目次を表示するページファイルのgetStaticPropsを下のように実装します。

export const getStaticProps = async (context: any) => {
  const id = context.params.id;
  const data = await client.get({ endpoint: "blogs", contentId: id });

  let toc = Array<TocH2>();  // 目次
  let h2Index = -1;  // 見出し2インデックス

  for (let i = 0; i < data.article_body.length; i++) {
    const body = data.article_body[i];

    // fieldIdの種類によって処理を分ける
    if (body.fieldId == "rich_editor") {
      // Rich Editorの場合は、見出しを抽出
      const $ = load(body.rich_text);
	  
      // リッチエディタのテキストからh2, h3の要素を抽出
      $("h2, h3").each((_, elm) => {
        if ($(elm).prop("tagName") == "H2") {
          // h2の要素からid, textを取得し、配列に設定
          h2Index += 1;
          const tocH2: TocH2 = {
            id: String($(elm).attr("id")),
            text: $(elm).text(),
            h3: Array<TocH3>(),
          };
          toc[h2Index] = tocH2;
        }

        if ($(elm).prop("tagName") == "H3" && h2Index != -1) {
          // h3の要素からid, textを取得し、配列に設定
          // h3の前に最初のh2がない場合は処理をスキップする
          const tocH3: TocH3 = {
            id: String($(elm).attr("id")),
            text: $(elm).text(),
          };
          toc[h2Index].h3.push(tocH3);
        }
      });
    }
  }

  return {
    props: {
      blog: data,
      toc: toc,
    },
  };
};

export default functionの実装

目次を表示するページファイルのexport default functionを下のように実装します。

type Props = {
  blog: Blog;
  toc: Array<TocH2>;
};

export default function BlogId({ blog, toc }: Props) {
  return (
    <div>
      {/* 目次 */}
      {blog.is_hide_toc ? null : (
        <div className="bl_toc">
          <div className="bl_toc_title">目次</div>
          <ul className="bl_toc_list">
            {toc.map((h2) => (
              <li className="bl_toc_list_item" key={h2.id}>
                <a href={`#${h2.id}`}>{h2.text}</a>
                {h2.h3.length == 0 ? null : (
                  <ul className="bl_toc_childList">
                    {h2.h3.map((h3) => (
                      <li className="bl_toc_childList_item" key={h3.id}>
                        <a href={`#${h3.id}`}>{h3.text}</a>
                      </li>
                    ))}
                  </ul>
                )}
              </li>
            ))}
          </ul>
        </div>
      )}
      {/* ブログ本文 */}
      {blog.article_body?.map((content) => (
        <div key={content.fieldId}>
          {content.fieldId === "rich_editor" ? (
            <div
              dangerouslySetInnerHTML={{
                __html: `${content.rich_text}`,
              }}
            />
          ) : null}
        </div>
      ))}
    </div>
  );
}

CSSの実装

参考までに目次イメージとして見せた下の画像のCSSはこちらになります。

.bl_toc {
  background-color: #f0f1f3;
  margin-top: 24px;
  padding: 6px 16px;
}

.bl_toc_title {
  font-weight: 700;
  line-height: 2;
  margin-left: 8px;
}

.bl_toc_list {
  margin: 0;
  padding-left: 0px;
}

.bl_toc_list_item {
  list-style: none;
  padding-left: 8px;
  margin-bottom: 7px;
}

.bl_toc_list_item a {
  text-decoration: none;
}

.bl_toc_childList {
  padding-left: 10px;
}

.bl_toc_childList_item {
  list-style: "-";
  line-height: 1.6;
  padding-left: 6px;
}

.bl_toc_childList_item a {
  text-decoration: none;
}

おわりに

繰り返しフィールドの要素から入れ子の目次を生成する方法について説明しました。目次を自動生成することで、記事全体の見通しが良くなり読み手の負担も軽減できると思います。

サイトを作成する際の参考となれば幸いです。

参考

このブログは以下の情報を参考にしました。

関連タグの記事

Ryota Kondo
Ryota Kondo

システムエンジニア・プログラマー|このブログサイトの運営もしており、思いついたことをまとめて記事を書いています💡