Linuxを日常的に使う実験ブログ

Astroで作ったブログ記事に目次をつける方法

 2024-07-07

 2024-07-07

 プログラミング

Astroで作ったブログに目次を追加したい

筆者はこのブログとは別にLinuxを日常使いにする実験を記事にしているクロの思考ノートを運営しています。最近WordPressからAstroに移行しました。記事に目次を入れようとしたのですが、少し戸惑う点もあったので記事化しておこうと思います。

生成したいHTML

こんな感じのH2の見出しとH3の見出しが入れ子になっているHTMLを動的に生成したいというのが最終的な目標です。

<ol>
  <li>H2の見出し1</li>
  <li>H2の見出し2
    <ul>
      <li>H3の見出し1</li>
      <li>H3の見出し2</li>
      <li>H3の見出し3</li>
    </ul>
  </li>
  <li>H2の見出し3
    <ul>
      <li>H3の見出し1</li>
      <li>H3の見出し2</li>
    </ul>
  </li>
  <li>H2の見出し4</li>
</ol>

Astro.propsから目次情報を取得する

Markdownでコンテンツを管理している場合はAstro.props.headingsから見出し情報を取得できます。取得できる形式は以下のようなものです。

取得データ例

[
  { depth: 2, slug: '前提', text: '前提' },
  {
    depth: 2,
    slug: 'fcitxとfcitx-mozcの導入',
    text: 'Fcitxとfcitx-mozcの導入'
  },
  { depth: 2, slug: '環境変数の設定', text: '環境変数の設定' },
  { depth: 2, slug: 'fcitxの起動', text: 'fcitxの起動' },
  { depth: 2, slug: '各種設定', text: '各種設定' },
  { depth: 3, slug: 'インプットメソッドエンジン', text: 'インプットメソッドエンジン' },
  { depth: 3, slug: 'fcitxのキーマップの変更', text: 'fcitxのキーマップの変更' },
  { depth: 3, slug: 'mozcのキーマップ変更', text: 'Mozcのキーマップ変更' },
  { depth: 2, slug: 'まとめ', text: 'まとめ' }
]

このデータを利用すれば目次を作成することが可能です。しかし、ご覧の通り、見出しがH2タグかH3タグなのかはdepth要素の数値で表され、H2タグとH3タグが入り混じった状態のため使うには不便です。

データを変換

前述の通り、Astro.props.headingsから取得できるデータは入れ子のHTMLを生成するには不便なので加工します。また、見出しのタグがH2, H3が入り混じった場合と記事中にH3タグしか存在しないのみの場合分けもしておきます。以下のようなJavascriptプログラムで加工します。

src/layouts/page.astro

function createIndex() {
  let index = []
  const depth = checkDepth();
  for (const item of headings) {
    if (depth === 2) {
      if (item.depth === 2) {
        index.push({
          slug: item.slug,
          text: item.text,
          child: []
        });
      } else if (item.depth === 3) {
        index[(index.length - 1)].child.push({
          slug: item.slug,
          text: item.text,
          chilid: []
        });
      }
    } else if (depth === 3) {
      if (item.depth === 3) {
        index.push({
          slug: item.slug,
          text: item.text,
          child: []
        })
      }
    }
  }
  return index
}

function checkDepth() {
  for (const item of headings) {
    if (item.depth === 2) {
    return 2;
    }

    return 3;
  }
}

入れ子のHTMLを生成する

Astroで動的に入れ子のHTMLを作るためには子HTML側をコンポーネント化する必要があります。まずは子コンポーネントを生成します。

components/index-child.astro

---
const {children} = Astro.props;

---

{children.length > 0 && <ul>{children.map(item => <li><a href={`#${item.slug}`}>{item.text}</a></li>)}</ul>}

次に目次を挿入したい記事用のレイアウトファイルに記事生成のスクリプトを記述します。今回はlayouts/post.astroファイルとします。

src/layouts/post.astro

---
...
import IndexChild from '../components/index-child.astro';
---
...
    <div class="index-wrapper">
      <input id="index-button" type="checkbox" checked />
      <div class="index-header">目次 [<label class="label" for="index-button"></label>]</div>
      <div class="index-content">
        <ol>
          {createIndex().map(item => <li><a href={`#${item.slug}`}>{item.text}</a><IndexChild children={item.child} /></li>)}
        </ol>
      </div>
    </div>
...

ポイントとしてはH2タグの見出しが更にH3タグを子要素として持っていた場合に子コンポーネントに要素を渡す部分でしょうか。最初、子コンポーネントを使わずHTMLを生成しようとしましたが、執筆時点のAstroではそれは無理だったようです。

これで以下ようなHTMLが生成されます。

生成されるHTML例

<div class="index-content">
  <ol>
    <li><a href="#前提">前提</a></li>
    <li><a href="#fcitxとfcitx-mozcの導入">Fcitxとfcitx-mozcの導入</a></li>
    <li><a href="#環境変数の設定">環境変数の設定</a></li>
    <li><a href="#fcitxの起動">fcitxの起動</a></li>
    <li><a href="#各種設定">各種設定</a>
      <ul>
        <li><a href="#インプットメソッドエンジン">インプットメソッドエンジン</a></li>
        <li><a href="#fcitxのキーマップの変更">fcitxのキーマップの変更</a></li>
        <li><a href="#mozcのキーマップ変更">Mozcのキーマップ変更</a></li>
      </ul>
    </li>
    <li><a href="#まとめ">まとめ</a></li>
  </ol>
</div>

まとめ

動的に入れ子のHTMLを作るという部分に躓きポイントがあったため、記事化しました。Astroは便利なSSGですが、目次生成はちょっと不便だったなというのが正直な感想です。もっと便利な方法あるよという方是非教えてください。