monaka

Next.jsで目次を作る(cheerio)

Next.jsで目次を作る方法を紹介する。cheerioを利用している。

完成イメージ

事前準備

cheerioというライブラリをインストールする。

npm install --save cheerio

目次コンポーネント作成

目次のコンポーネントを作成する。Table of ContentsなのでToCディレクトリを作成した。ToCディレクトリの配下に、「index.tsx」、「index.module.css」を作成する。まず、「index.tsx」については以下のように記述する

import React from "react";
import styles from "./index.module.css";

interface TocItem {
  id: string;
  text: string;
}

interface TableOfContentsProps {
  toc: TocItem[];
}

const TableOfContents: React.FC<TableOfContentsProps> = ({ toc }) => {
  if (toc.length === 0) {
    return null;
  }

  return (
    <div className={styles.toc}>
      <h2 className={styles.h2}>目次</h2>
      <ul>
        {toc.map((item) => (
          <li className={styles.tocli} key={item.id}>
            <a href={`#${item.id}`}>{item.text}</a>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TableOfContents;

続いて「index.module.css」については以下のように記述した。デザインはmicroCMSブログをリスペクトしているので、素人ながらできる限り寄せてみた(必要に応じて変更)

.toc {
  background-color: #f7f7fc;
  padding: 20px;
  margin-bottom: 20px;
}

.tocli {
  border-bottom: 1px solid #e7e7f3;
  padding: 5px 0;
  font-size: 14px;
}

.tocli:before {
  color: #cacae7;
  content: "-";
  margin-right: 5px;
}

.h2 {
  font-size: 1rem;
}

記事(article)側で呼び出す

コンポーネント化できたらあとは目次部分を呼び出す。自分はシンプルなコーポレートサイト(microCMSテンプレート)を改変したものを利用しているので、記事部分がコンポーネント化されている。今回はarticleのindex.tsxを編集した。最終的に以下のようになっている。

import Link from "next/link";
import Image from "next/image";
import type { News } from "@/app/_libs/microcms";
import Date from "../Date";
import Category from "../Category";
import TableOfContents from "../ToC"; // ToCコンポーネントをインポート
import styles from "./index.module.css";
import * as cheerio from "cheerio";

type Props = {
  data: News;
};

export default function Article({ data }: Props) {
  // data.contentがundefinedまたは空の場合のハンドリング
  if (!data || !data.content) {
    return <div>記事が見つかりませんでした。</div>; // エラーハンドリング
  }

  // Cheerioを使って目次を生成
  const $ = cheerio.load(data.content);
  const toc: { id: string; text: string }[] = [];

  // h2 と h3 見出しを解析
  $("h2, h3").each((_, element) => {
    const id =
      $(element).attr("id") ||
      $(element).text().replace(/\s+/g, "-").toLowerCase();
    const text = $(element).text();

    if (!$(element).attr("id")) {
      $(element).attr("id", id); // idを追加
    }

    toc.push({ id, text });
  });

  return (
    <main>
      {data.thumbnail && (
        <Image
          src={data.thumbnail.url}
          alt=""
          className={styles.thumbnail}
          width={data.thumbnail.width}
          height={data.thumbnail.height}
        />
      )}
      <h1 className={styles.title}>{data.title}</h1>
      <div className={styles.meta}>
        <Link
          href={`/blog/category/${data.category.id}`}
          className={styles.categoryLink}
        >
          <Category category={data.category} />
        </Link>
        <Date date={data.publishedAt ?? data.createdAt} />
      </div>

      {/* 目次を表示 */}
      <TableOfContents toc={toc} />

      <div
        className={styles.content}
        dangerouslySetInnerHTML={{
          __html: data.content,
        }}
      />
    </main>
  );
}

補足事項

microCMSブログに掲載されているNext.js で目次機能を実装してみようを参考にしてみたが、自分の環境だとうまくいかなかったためアレンジをしてしまっている。

Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門 高速で、安全で、運用しやすいサイトのつくりかた
Next.js+ヘッドレスCMSではじめる! かんたんモダンWebサイト制作入門 高速で、安全で、運用しやすいサイトのつくりかた
Amazon楽天市場Yahoo!ショッピング

関連記事