こんにちは!リードエンジニアをしているkoojyです。
僕は日々フロントエンドの開発を行っています。

今回はAstroでMDXを使って、任意の場所に目次を追加する方法を紹介したいと思います。
Astroでは記事の目次を表示する方法はいくつかありますが任意の場所に表示する方法はなかなか見つからなかったので記事にしてみます。

この記事を通じて、AstroとMDXを使用してみなさんのやりたいことを実現するためのヒントを提供できればと思います。

目次の重要性

まず目次はユーザーがコンテンツに簡単にアクセスできるようにするための重要な要素です。
特に、長いページや複数のセクションを持つページでは、目次を提供することがユーザーエクスペリエンスの向上につながります。
目次を表示することで、ユーザーはコンテンツ全体の概要を把握しやすくなり、必要な情報に素早くアクセスできるようになります。

実装にあたり前提条件

今回の記事を理解して実装するためには、以下の前提条件を満たしていることが望ましいです。

  • Astroプロジェクトがセットアップされている。
  • MDXのことを知っている。

Astroで目次を表示する方法

Astroで目次を表示する方法は調べていると2つの方法をよく見かけました。

  • Contentコンポーネントとは別に目次を表示するコンポーネントを配置する
  • remark-tocを使用して目次を生成する

今回はこの2つの方法には詳しく触れませんが、どちらの方法も目次を表示するための有効な手段ではありながら任意の位置に目次を表示するという目的を満たすことができませんでした。

目次のコンポーネントを作成する

まずは目次のコンポーネントを2つ作成します。

  • Toc.astro:目次全体を表示するコンポーネント
  • TocItem.astro:目次の各項目を表示するコンポーネント
---
import type { MarkdownHeading } from 'astro'
import TocItem from './TocItem.astro'

interface Props {
  headings: MarkdownHeading[]
}

function buildHierarchy(headings: MarkdownHeading[]) {
  const toc: {
    depth: number
    subheadings: MarkdownHeading[]
    slug: string
    text: string
  }[] = []
  const parentHeadings = new Map()

  headings.forEach((h: MarkdownHeading) => {
    const heading = { ...h, subheadings: [] }

    parentHeadings.set(heading.depth, heading)

    if (heading.depth === 2) {
      toc.push(heading)
    } else {
      parentHeadings.get(heading.depth - 1).subheadings.push(heading)
    }
  })

  return toc
}

const headings = buildHierarchy(Astro.props.headings ?? [])
---

<ol>
  {
    headings.map((heading) => {
      return <TocItem heading={heading} />
    })
  }
</ol>
---
const { heading } = Astro.props
---

<li>
  <a href={'#' + heading.slug}>
    {heading.text}
  </a>
  {
    heading.subheadings.length > 0 && (
      <ol>
        {heading.subheadings.map((subheading: any) => (
          <Astro.self heading={subheading} />
        ))}
      </ol>
    )
  }
</li>

今回目次のコンポーネントを作成するにあたり下記の記事を参考にさせていただきました。

https://medium.com/@rezahedi/how-to-build-table-of-contents-in-astro-and-sectionize-the-markdown-content-78bee84e6a7f

ContentのcomponentsにTOCを定義する

MDXなので記事のファイル内からimportすることもできますが、今回はContentコンポーネントのcomponentsにTOCを定義していきます。

---
import TOC from 'path/to/toc.astro' // 定義したTOC.astroを読み込み
---

<Content components={{
	TOC,
}} />

これで記事内からTOCコンポーネントを呼び出すことができるようになります。

目次を任意の場所に追加する

準備が整ったら、目次を表示したいページのMDXファイルに以下のように記述します。

<TOC headings={getHeadings()} />

TOCは先程定義したコンポーネントです。
ここでgetHeadings()を呼び出していますが、これはページ内の見出し情報を取得する関数です。

Astroでは記事内で使える変数や関数が用意されていてgetHeadingsはその1つです。

https://docs.astro.build/ja/guides/markdown-content/#%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88%E3%81%97%E3%81%9F%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3

getHeadingsは下記のオブジェクトの配列を返します。

{
  depth: number // 見出しのレベル
  slug: string // 見出しのID
  text: string // 見出しのテキスト
}

これで任意の位置にTOCを記述することで、目次を意図した場所に表示することができます。

まとめ

目次の表示はユーザーエクスペリエンスを向上させ、コンテンツのナビゲーションを容易にする効果的な方法です。

この記事で紹介したステップを実践することで、みなさんのウェブサイトやアプリケーションにおいて、読者が求める情報を迅速に見つけられるようになります。
今回紹介した方法を基に、さらにカスタマイズや最適化を行い、より良いユーザー体験の提供を目指しましょう。