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のセットアップについて知りたい場合は、以下のドキュメントをご確認ください。
- Next.jsの Getting Started のドキュメント
- NewtとNext.jsを利用してブログを作成する
概要
Newtから返却されるHTMLデータ(リッチテキストフィールド・マークダウンフィールド)の内容をもとに、見出し(h2要素)を抜き出して、目次を作成します。
以下のステップで説明します。
- HTMLデータの見出しにidを設定する
- 目次を作成し、見出しに遷移できるようにする
フロントエンド側でidを設定する必要があります。
cheerioをインストールする
まずは、cheerio をインストールします。
## npmを利用している場合
npm install --save cheerio
## yarnを利用している場合
yarn add cheerio
HTMLデータの見出しにidを設定する
ここから、目次を作成したいページを編集していきます。
もともと以下のような投稿詳細ページがあるとし、article.body
にリッチテキストフィールドまたはマークダウンフィールドが設定され、HTMLデータが返却されるものとします。
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を設定する処理を追加していきます。
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>
)
}
処理内容の詳細は以下の通りです。
- cheerioの load 関数に
article.body
を渡すことでHTMLのパースを行います $('h2').each((_, elm) => { /* 変換処理 */ })
として、h2要素のみを抽出して順次処理を実行します$(elm).text()
として、抽出した要素からテキストコンテンツを取得します- 抽出した要素のコンテンツをaタグで囲みます(id属性とhref属性に3で取得したテキストコンテンツを設定します)
5.$.html()
を実行して、パースしたDOMノードを再びHTMLへと変換してarticle.body
に再代入します
これでh2要素のテキストコンテンツをaタグでラップし、idを設定することができました。
以下のように、見出し部分がリンクに変わっていることがわかります。
見出し部分の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>
)
}
ここでは以下のことを行っています。
- h2要素のテキストコンテンツを抽出し、
headings
という配列を作成しています headings
の情報をもとに、リストを作成し、各見出しに対応するアンカーリンクを設定しています
スタイルについて、ここでは styles.TableOfContents
で定義していますが、お好みで設定してください。
以下のように、目次が作成されていれば成功です。
まとめ
このようにcheerioを利用することで、jQueryのようなAPIを使いながらHTMLを柔軟に変換することができました。
このチュートリアルではh2要素のみを抽出するシンプルな目次を作成しましたが、複数の要素を指定した目次を作成したり、さらには脚注を作成するといった、さらに踏み込んだカスタマイズもできますので、ぜひトライしてみてください。