microCMS

Astro × microCMS × cheerioで目次機能を実装する

はじめに

はじめまして、株式会社メンバーズの大西です。
普段はWebエンジニアとして、ヘッドレスCMSやAstroなどを使用したJamstack構成の開発を中心に取り組んでいます。

本記事では、microCMSから取得した記事データを元に、Astroとcheerioを使って目次(TOC: Table of Contents)を生成する方法について解説していきます。
目次があれば、読者は記事の全体像を一目で把握でき、知りたい情報にスムーズにアクセスできるようになるため、読者の離脱を防ぎやすくなります。

本記事の最終的な完成形は以下のイメージです。

事前準備

今回使用する主な技術スタックとバージョンは以下の通りです。

  • Astro: v5.7
  • cheerio: v1.0


cheerioは軽量で高速なHTML/XMLパーサーライブラリで、今回はmicroCMSから取得したHTMLコンテンツの中にある見出しタグを抽出するために使用します。

なお本記事は、AstroとmicroCMSのチュートリアル「AstroとmicroCMSでつくるブログサイト」の拡張版になります。本記事はこのチュートリアルを終えている前提で話を進めていきます。

実装手順

microCMSで記事を入稿

まずはじめに、microCMSで記事を入稿します。

本文(フィールドID:content)のリッチエディタで「見出し2」と「見出し3」を利用して記事を作成してください(記事タイトルで h1 を使用する前提なので、本文では「見出し2」と「見出し3」を使用します)。

microCMSのリッチエディタでは、見出しを設定すると自動的にid属性が付与されます。このid属性を後ほど目次のアンカーリンクとして使用します。

cheerioのインストール

続いて、プロジェクトにcheerioをインストールします。

npm install cheerio


cheerioを使ってHTMLから見出しを抽出

cheerioを使って記事ページの見出し要素を抽出します。

// /src/pages/blog/[blog_id].astro

---
import * as cheerio from 'cheerio';
import { getBlogs } from '../../library/microcms.js';
import Layout from '../../layouts/Layout.astro';
import Toc from '../../components/Toc.astro';

export async function getStaticPaths() {
  const response = await getBlogs({ fields: ['id', 'title', 'content'] });

  return response.contents.map((content) => ({
    params: {
      blog_id: content.id,
    },
    props: { content },
  }));
}

const { content } = Astro.props;

const cheerioDom = cheerio.load(content.content);
const headings = cheerioDom('h2, h3').toArray();
const toc = headings.map((data) => ({
  id: data.attribs.id,
  text: cheerioDom(data).text(),
  tag: data.tagName,
}));
---

<Layout title={content.title}>
  <Toc tocItems={toc} />
  <div set:html={content.content} />
</Layout>


まず、cheerio.load() を使い、microCMSから取得したHTMLコンテンツをcheerioで解析可能なオブジェクトに変換します。

次に、そのオブジェクトから h2, h3タグを指定し、目次となる見出し要素を取得します。取得した各見出し要素からidtext(見出しテキスト)、tag(タグ名)を抽出し、それらを一つの配列にまとめていきます。

body内では、先ほど抽出した見出しデータの配列を、次に作成するTocコンポーネントに渡します。

Tocコンポーネントを作成し、抽出した見出しを階層構造に変換

次に、抽出した見出しデータを階層構造に変換して表示するTocコンポーネントを作成します。

// /src/components/Toc.astro

---
export interface Props {
  tocItems: Array<{
    id: string;
    text: string;
    tag: string;
  }>;
}

type TocItem = {
  id: string;
  text: string;
  tag: string;
  children?: TocItem[];
};

const { tocItems } = Astro.props;

function buildNestedToc(items: Props['tocItems']): TocItem[] {
  const result: TocItem[] = [];
  let currentH2: TocItem | null = null;

  for (const item of items) {
    if (item.tag === 'h2') {
      currentH2 = {
        ...item,
        children: [],
      };
      result.push(currentH2);
    } else if (item.tag === 'h3' && currentH2) {
      currentH2.children!.push(item);
    }
  }

  return result;
}

const nestedToc = buildNestedToc(tocItems);
---

<nav class="toc">
  <p class="toc-title">目次</p>
  <ul class="toc-list">
    {nestedToc.map((item) => (
      <li class={`toc-item ${item.tag}`}>
        <a href={`#${item.id}`} class="toc-link">
          {item.text}
        </a>
        {item.children && item.children.length > 0 && (
          <ul class="toc-sublist">
            {item.children.map((child: TocItem) => (
              <li class={`toc-item ${child.tag}`}>
                <a href={`#${child.id}`} class="toc-link">
                  {child.text}
                </a>
              </li>
            ))}
          </ul>
        )}
      </li>
    ))}
  </ul>
</nav>


buildNestedToc 関数でフラットな見出しの配列を階層構造に変換します。
h2 要素を親として、その後に続く h3 要素を子要素として配置します。具体的な変換前と変換後の配列データは以下のようになります。

変換前(tocItems

[
  { id: 'hca1f775802', text: '章1タイトル', tag: 'h2' },
  { id: 'hfe276746c3', text: 'セクション1-1', tag: 'h3' },
  { id: 'hf89c3e355c', text: 'セクション1-2', tag: 'h3' },
  { id: 'h79eec94d79', text: '章2タイトル', tag: 'h2' },
  { id: 'h57adfb5530', text: 'セクション2-1', tag: 'h3' }
]


変換後(nestedToc

[
  {
    "id": "hca1f775802",
    "text": "章1タイトル",
    "tag": "h2",
    "children": [
      {
        "id": "hfe276746c3",
        "text": "セクション1-1",
        "tag": "h3"
      },
      {
        "id": "hf89c3e355c",
        "text": "セクション1-2",
        "tag": "h3"
      }
    ]
  },
  {
    "id": "h79eec94d79",
    "text": "章2タイトル",
    "tag": "h2",
    "children": [
      {
        "id": "h57adfb5530",
        "text": "セクション2-1",
        "tag": "h3"
      }
    ]
  }
]


そして、変換後の配列をAstroのテンプレート内でリストとしてレンダリングします。
a タグの href 属性に id を設定して、目次からアンカーリンクとしてジャンプできるようにします。

スタイルの調整

目次にCSSを追加し、スタイルを調整します。h2h3 でスタイルを分けたり、インデントを付けることで階層構造を分かりやすくします。

// /src/components/Toc.astro

<style>
  .toc {
    background-color: #f9f9f9;
    border: 1px solid #ddd;
    padding: 15px;
    margin-bottom: 20px;
    border-radius: 5px;
  }

  .toc-title {
    font-size: 1.2em;
    font-weight: bold;
    margin-top: 0;
    margin-bottom: 10px;
  }

  .toc-list {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  .toc-sublist {
    list-style: none;
    padding-left: 20px;
    margin-top: 5px;
    margin-bottom: 0;
  }

  .toc-item.h2 {
    font-weight: bold;
    margin: 10px 0;
  }

  .toc-item.h3 {
    font-weight: normal;
    margin: 5px 0;
  }

  .toc-link {
    text-decoration: none;
    color: #007acc;

  }

  .toc-link:hover {
    text-decoration: underline;
  }
</style>


目次の表示/非表示をmicroCMS側から制御する

ここまでで目次の表示はできるようになったので、目次の表示/非表示をmicroCMS側から設定できるようにしていきます。
この実装を行うことで、デフォルト設定では目次を表示し、短い記事では手動で目次を非表示にするなど、記事ごとに使い分けが可能になります。

まずは、microCMSの管理画面で、記事ごとに目次の表示/非表示を制御できるように真偽値のフィールドを追加します。

ブログコンテンツの「API設定」→「APIスキーマ」で真偽値のフィールドを追加し、フィールドIDは insert_toc で設定します。デフォルトで目次を表示する場合は、詳細設定で初期値の設定が可能です。

「目次の挿入」のトグルボタンが追加されていることを確認し、ボタンをオンに設定しておきます。

続いて、ソースコード側のブログ記事ページで、microCMSから insert_toc フィールドを取得し、条件分岐で目次の表示を制御する実装を追加します。ブログ記事の型定義も修正する必要があります。

// /src/pages/blog/[blog_id].astro

const response = (await getBlogs({ fields: ["id", "title", "content", "insert_toc"] }))

getBlogs 関数のフィールドに insert_toc を追加します。

// /src/pages/blog/[blog_id].astro

{content.insert_toc && <Toc tocItems={toc} />}

insert_toctrue の場合のみ Toc コンポーネントを表示するように修正します。

この実装によりmicroCMSの管理画面から目次の表示/非表示を簡単に制御できるようになりました。
本記事の最終的なアウトプットは以下の通りです。

まとめ

本記事でご紹介した実装により、階層構造を持つ目次を生成できるようになりました。
また、microCMSの管理画面から目次の表示/非表示を簡単に設定することができます。
cheerioはHTML/XMLを解析・操作できるため、今回の目次生成以外にも様々な場面で活用できます。

私自身も記事コンテンツ内にスタイリング用の要素を追加する用途で使用した経験がありますが、
その他にも、img タグの src 属性をカスタマイズして画像最適化を行ったり、コードブロック( pre タグ)にコピーボタンを自動挿入したりと、様々な活用方法があります。

このように、cheerioのDOM操作・解析機能を活用することで、microCMSから取得したコンテンツを柔軟に加工・最適化できるようになります。
皆さんもぜひ活用してみてください。

参考

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

大西薫
株式会社メンバーズ メンバーズルーツカンパニーでエンジニアをしています。最近はJamstack構成の開発を中心に行っています。
株式会社メンバーズ 認定パートナー
運用で社会を変革するデジタルクリエイター集団です。弊社は、microCMSを活用して、国内金融企業さまへの『デジタル運用特化』でお客様のビジネス変革・成果向上の内製化を推進します。
https://www.members.co.jp/
制作を依頼する