2023.06.22

microCMSで書いたコンテンツに目次を追加してみたよ

開発環境

  • Next.js ^13.4.4
  • cheerio ^1.0.0-rc.12

完成コード

export const createTableOfContents = (richText: string) => {
  const $ = cheerio.load(richText);
  const headings = $('h1, h2, h3').toArray();
  const tableOfContents = headings
    .map((heading) => {
      if (heading.type === 'tag') {
        const id = heading.attribs.id;
        const name = heading.name;
        const text = $(heading).text();
        return { id, name, text };
      }
    })
    .filter(Boolean);
  return tableOfContents;
};
export default function Article({ data }: Props) {
  const tableOfContents = createTableOfContents(data.content);
  return (
    {/* 省略 */}
          {tableOfContents.length > 0 && (
        <div className={styles.toc}>
          <p>目次</p>
          <ul>
            {tableOfContents.map((toc) => (
              <li key={toc?.id} className={styles[`toc-${toc?.name}`]}>
                <Link href={`#${toc?.id}`}>{toc?.text}</Link>
              </li>
            ))}
          </ul>
        </div>
      )}
        {/* 省略 */}
  )
}

完成イメージ

解説

cheerioを使ってHTMLをパースし、目次になるタグを取得する

🤔 HTMLをパースするとは?

<h1>で囲まれたテキストは見出しですよ」
<p>で囲まれたテキストは段落ですよ」
という感じで、HTML文書を解析して要素や構造を把握できるようにすることをいうよ✌🏻

const $ = cheerio.load(richText);

これはお決まりのような感じ。
パースの結果を$ に格納し、$を使ってHTMLを操作します。
jQueryライクなAPIなので$オブジェクトを使って取得するみたい。

const headings = $('h1, h2, h3').toArray();

HTMLから必要なタグを取得し、配列に格納します。
今回は目次なのでheadingの要素に絞り込み🐒

取得したタグから必要な情報を取り出し、返却用のデータを作成する

const tableOfContents = headings
    .map((heading) => {
      if (heading.type === 'tag') {
        const id = heading.attribs.id;
        const name = heading.name;
        const text = $(heading).text();
        return { id, name, text };
      }
    }).filter(Boolean);

map関数を使って各ヘッダー要素から必要な情報を取り出し、新しいオブジェクトを作成します。
今回取得した情報は以下の用途で使用するよ!

  • id:目次のhref属性

    microCMSでは各heading要素にあらかじめidが振られているので、目次の項目をクリックしたら該当箇所にページ内リンクできるようにします。

  • name:目次のclass名

    目次の階層構造を表現するために、name(h2、h3など)を取得してclass名を付与します。

  • text:目次の項目

.filter(Boolean); で、undefinedが返された要素、つまりタグではない要素を配列から排除するようにしているよ。

コンポーネントで呼び出す

const tableOfContents = createTableOfContents(data.content);

先ほど作った関数を呼び出してtableOfContentsに代入します。
data.contentの部分にはmicroCMSから取得したコンテンツをいれてね)

{tableOfContents.length > 0 && (
  <div className={styles.toc}>
    <p>目次</p>
    <ul>
      {tableOfContents.map((toc) => (
        <li key={toc?.id} className={styles[`toc-${toc?.name}`]}>
          <Link href={`#${toc?.id}`}>{toc?.text}</Link>
        </li>
       ))}
    </ul>
  </div>
)}

あとはtableOfContentsを使って目次を描写してあげれば完了!

CSSモジュールで動的なクラスをつけるにはどうしたらいいのかはChatGPTさまが教えてくれました。🙏🏻

<li key={toc?.id} className={styles[`toc-${toc?.name}`]}>

こんな感じでいいようです。

CSSも一応置いておく✌🏻

.toc {
  width: 100%;
  margin-bottom: 2.8em;
}

.toc p {
  display: inline-block;
  background: rgba(0, 0, 0, 0.8);
  color: #fff;
  border-radius: 4px 4px 0 0;
  padding: 5px 25px;
}

.toc ul {
  background: #f1f2f3;
  padding: 20px;
}

.toc li {
  margin-bottom: 5px;
}

.toc li a:hover {
  opacity: 0.7;
}

.toc-h3 {
  padding-left: 16px;
}

.toc-h4 {
  padding-left: 16px;
}

感想

目次つくるの結構大変そうと思ってたけど、cheerioがいい感じにお仕事してくれるし、
microCMSがheading要素に自動でidつけるようにしてくれていたから思ってたより簡単にできました😊

そのうち目次の表示位置を変えたいな〜〜

最後まで読んでいただきありがとうございます!
もしよければ「読んだよ!」の代わりに↑の紙飛行機をクリックで飛ばしてください。わたしの元に届きます。

Special Thanks!!!

ありがとうございました