cheerioを利用して、Next.js (App Router)・Newt製サイトに目次を作成する

最終更新日:

Table of contents

cheerio は、Node.js上でjQueryライクなAPIでHTMLをパース・操作できるJavaScriptライブラリです。このチュートリアルでは、cheerioを利用して、Next.js (App Router)・Newtで作られたサイトに、目次を追加する方法を紹介します。

記事内で使用している主なソフトウェアのバージョン

  • Next.js(next): 13.4.6
  • cheerio(cheerio): 1.0.0-rc.12

前提条件

  • Next.jsのプロジェクトを作成済みであること

Next.jsのセットアップについて知りたい場合は、以下のドキュメントをご確認ください。

概要

Newtから返却されるHTMLデータ(リッチテキストフィールド・マークダウンフィールド)の内容をもとに、見出し(h2要素)を抜き出して、目次を作成します。
以下のステップで説明します。

  • HTMLデータの見出しにidを設定する
  • 目次を作成し、見出しに遷移できるようにする
Newtから返却されるHTMLデータ(リッチテキストフィールド・マークダウンフィールド)では、現状タグにidは振られません。
フロントエンド側でidを設定する必要があります。

cheerioをインストールする

まずは、cheerio をインストールします。

## npmを利用している場合
npm install --save cheerio

## yarnを利用している場合
yarn add cheerio

HTMLデータの見出しにidを設定する

ここから、目次を作成したいページを編集していきます。
もともと以下のような投稿詳細ページがあるとし、article.body にリッチテキストフィールドまたはマークダウンフィールドが設定され、HTMLデータが返却されるものとします。

app/articles/[slug]/page.tsx
1import { getArticleBySlug } from '@/lib/newt'
2import styles from '@/app/page.module.css'
3
4type Props = {
5  params: {
6    slug: string
7  }
8}
9
10export default async function Article({ params }: Props) {
11  const { slug } = params
12  const article = await getArticleBySlug(slug)
13  if (!article) return
14
15  return (
16    <main className={styles.main}>
17      <h1>{article.title}</h1>
18      <div dangerouslySetInnerHTML={{ __html: article.body }} />
19    </main>
20  )
21}

ここに、見出しにidを設定する処理を追加していきます。

app/articles/[slug]/page.tsx
import { load } from 'cheerio'
import { getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'

type Props = {
  params: {
    slug: string
  }
}

export default async function Article({ params }: Props) {
  const { slug } = params
  const article = await getArticleBySlug(slug)
  if (!article) return

  const $ = load(article.body) // 1
  $('h2').each((_, elm) => { // 2
    const text = $(elm).text() // 3
    $(elm).contents().wrap(`<a id="${text}" href="#${text}"></a>`) // 4
  })
  article.body = $.html() // 5

  return (
    <main className={styles.main}>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.body }} />
    </main>
  )
}

処理内容の詳細は以下の通りです。

  1. cheerioの load 関数に article.body を渡すことでHTMLのパースを行います
  2. $('h2').each((_, elm) => { /* 変換処理 */ }) として、h2要素のみを抽出して順次処理を実行します
  3. $(elm).text() として、抽出した要素からテキストコンテンツを取得します
  4. 抽出した要素のコンテンツをaタグで囲みます(id属性とhref属性に3で取得したテキストコンテンツを設定します)
    5.$.html()を実行して、パースしたDOMノードを再びHTMLへと変換して article.body に再代入します

これでh2要素のテキストコンテンツをaタグでラップし、idを設定することができました。
以下のように、見出し部分がリンクに変わっていることがわかります。
cheerio_toc1.jpg

見出し部分のHTMLは、以下のようになっています。

<h2>
  <a id="ステップ1" href="#ステップ1">ステップ1</a>
</h2>

目次を作成し、見出しに遷移できるようにする

次に目次を作成し、見出しに遷移できるようにしましょう。
以下のコードを追加します。

(省略)

export default async function Article({ params }: Props) {
  const { slug } = params
  const article = await getArticleBySlug(slug)
  if (!article) return

  const headings: string[] = []

  const $ = load(article.body)
  $('h2').each((_, elm) => {
    const text = $(elm).text()
    headings.push(text)
    $(elm).contents().wrap(`<a id="${text}" href="#${text}"></a>`)
  })

  article.body = $.html()

  return (
    <main className={styles.main}>
      <h1>{article.title}</h1>
      <div className={styles.TableOfContents}>
        <h2>目次</h2>
        <ul>
          {headings.map((text, index) => {
            return (
              <li key={index}>
                <a href={`#${text}`}>{text}</a>
              </li>
            )
          })}
        </ul>
      </div>
      <div dangerouslySetInnerHTML={{ __html: article.body }} />
    </main>
  )
}

ここでは以下のことを行っています。

  1. h2要素のテキストコンテンツを抽出し、headings という配列を作成しています
  2. headings の情報をもとに、リストを作成し、各見出しに対応するアンカーリンクを設定しています

スタイルについて、ここでは styles.TableOfContents で定義していますが、お好みで設定してください。

以下のように、目次が作成されていれば成功です。
cheerio_toc2.png

まとめ

このようにcheerioを利用することで、jQueryのようなAPIを使いながらHTMLを柔軟に変換することができました。
このチュートリアルではh2要素のみを抽出するシンプルな目次を作成しましたが、複数の要素を指定した目次を作成したり、さらには脚注を作成するといった、さらに踏み込んだカスタマイズもできますので、ぜひトライしてみてください。

NewtMade in Newt