はなゐろぐ

主に技術関係の覚え書きです。

Astro Contents Collectionで目次を作る

2024年11月1日

はじめに

Astro Conents Collectionは見出しを配列にしてくれるので、それを使って階層状の目次を作ります。

目次を作る

見出しを取得する

記事詳細ページではgetStaticPaths()で各記事をMarkdownから取得しているので、render()headingsを取り出します。

---
// 〜前略〜
const { post } = Astro.props
const { Content, headings } = await post.render()
---

headingsの中にはこのような配列が格納されています。

[
  { depth: 2, slug: 'はじめに', text: 'はじめに' },
  { depth: 2, slug: '目次を作る', text: '目次を作る' },
  { depth: 3, slug: '見出しを取得する', text: '見出しを取得する' }
]

これをこれから作成する目次コンポーネントに渡します。

目次コンポーネントを作る

私が作ったのはこんな感じのコンポーネントです。headingsは階層化されていないため、新しく配列を作って小見出しを大見出しのsubheadingsに格納します。Astro.selfを使っているのがミソで、小見出しの目次は自分自身(コンポーネント)を呼び出して同じ処理を繰り返します。こうすることで、簡単に入れ子のリストを作れます。見出しレベル4より上は含めないようにしていますが、これは好みで変更して構いません。

TableOfContents.astro
---
import { MarkdownHeading } from 'astro'

interface HeadingWithSubheadings extends MarkdownHeading {
  depth: number
  subheadings: MarkdownHeading[]
}

export interface Props {
  headings: MarkdownHeading[]
  depth?: number
}

const { headings, depth = 2 } = Astro.props

const hierarchy = headings.reduce((array, heading) => {
  if (depth > 4) {
    return array
  }

  if (heading.depth === depth) {
    array.push({ ...heading, subheadings: [] })
  } else {
    array.at(-1)?.subheadings.push(heading)
  }

  return array
}, [] as HeadingWithSubheadings[])
---

{
  hierarchy && (
    <ol class={depth > 2 && 'pl-4'}>
      {hierarchy.map(({ slug, text, subheadings }) => (
        <li class="">
          <a href={`#${slug}`}>{text}</a>
          {subheadings.length > 0 && (
            <Astro.self headings={subheadings} depth={depth + 1} />
          )}
        </li>
      ))}
    </ol>
  )
}