raahii.meのブログのロゴ画像

ウェブログ

Astro のブログに目次を追加する

前回書いた Astro でブログをリニューアルした の記事では、Astro を使ってブログを作成する方法を概観しました。この記事では rehype-toc を使って、ブログ記事に目次を追加する方法を紹介します。

プロジェクトの作成

目次を追加する流れを説明するために、まずは新規プロジェクトを作成します。

$ npm init astro -- --template blog
$ tree -L 4
.
├── README.md
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
│   └── ...
├── src
│   ├── components
│   │   ├── BaseHead.astro
│   │   ├── Footer.astro
│   │   ├── FormattedDate.astro
│   │   ├── Header.astro
│   │   └── HeaderLink.astro
│   ├── consts.ts
│   ├── content
│   │   ├── blog
│   │   │   ├── first-post.md
│   │   │   ├── markdown-style-guide.md
│   │   │   ├── second-post.md
│   │   │   ├── third-post.md
│   │   │   └── using-mdx.mdx
│   │   └── config.ts
│   ├── env.d.ts
│   ├── layouts
│   │   └── BlogPost.astro
│   ├── pages
│   │   ├── about.astro
│   │   ├── blog
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── index.astro
│   │   └── rss.xml.js
│   └── styles
│       └── global.css
└── tsconfig.json

テンプレートを使ったので、シンプルなマイクロブログが既に出来上がった状態になります。立ち上げて記事を見てみます。

$ npm run dev
テンプレートを使って生成したシンプルなマイクロブログのTOPページ

テスト用の記事を追加する

src/content/blog/test.md に新しい記事を作成して、この記事でテストしていきます。h2h3 の見出しを追加します。

---
title: "Table of Contents Test"
description: "This is an post to test TOC"
pubDate: "Jul 22 2022"
---

## Introduction

## Body
### Main
### Second

## Conclusion

Hello, World!
目次をこれから実装していくテスト記事のプレビュー画像

rehype のプラグインをインストールする

次に、目次を実装するための rehype のプラグインをインストールします。

$ npm i rehype-slug rehype-toc

rehypeとは HTML のプロセッサです。Markdown から出力された HTML から目次を生成します。

Astro では rehype のプラグインを簡単に導入できます。astro.config.mjs に設定を追加するだけで、記事の冒頭に目次が生成されるようになります。

 ...
 export default defineConfig({
   site: "https://example.com",
   integrations: [mdx(), sitemap()],
+  markdown: {
+    rehypePlugins: [
+      "rehype-slug",
+      ["rehype-toc", { headings: ["h2", "h3"] }],
+    ],
+  },
 });

今回は、記事内では h2, h3 のみが使われる想定で、それらのみを対象に目次を作成しています。

目次のHTML要素は <nav class="toc"> として生成されるので、見た目も少し調整しておきます。src/styles/global.css を編集します。

...
blockquote {
  border: 1px solid #999;
  color: #222;
  padding: 2px 0px 2px 20px;
  margin: 0px;
  font-style: italic;
}
+
+.toc {
+  border: 2px solid #efefef;
+}
+
+.toc ol {
+  list-style: none;
+  counter-reset: toc;
+  padding-left: 32px;
+}
+
+.toc ol li {
+  counter-increment: toc;
+}
+
+.toc ol li:before {
+  content: counters(toc, ".") " ";
+}

枠線を追加し、字下げに対応して番号を振るようにしてみました。

テスト記事に目次を追加した結果の画像

簡単で素晴らしいですね、完了です!

目次にスタイルをあてる際に style タグの使い方には注意してください。Astro での style タグは、デフォルトでそのページだけに適用される scoped なスタイルとなりますが、目次はあくまで記事の一部として生成されており、ページ単位のクラスが付与されないため適用されません。

  ...
  <article class="astro-BVZIHDZO">
    <h1 class="title astro-BVZIHDZO">Table of Contents Test</h1>
    ...
    <!-- ここからが <Content /> の要素 -->
    <nav class="toc"> <!-- astroーXXXX のようなクラスが振られない -->
      <ol class="toc-level toc-level-1">
        ...

よってglobal.css に書くか、 <style is:global> を使ってグローバルスタイルにするのであればタグを使っても大丈夫だと思います。

目次の表示を制御する

ここからの内容はお好みになります。まず、記事によっては目次が不要なこともあるので、記事毎に目次を表示するか設定できるようにしたいと思います。

今回は src/content/blog/test.md の記事だけ表示したいと仮定します。記事の frontmatter に showToc を追加します。

 ---
 title: "Table of Contents Test"
 description: "This is an post to test TOC"
 pubDate: "Jul 22 2022"
+showToc: true
 ---
 ...

デフォルトでは目次が表示される状態なので、src/layouts/BlogPost.astroshowToc に応じて非表示にします。

 ---
 ...

-const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
+const { title, description, pubDate, updatedDate, heroImage, showToc } = Astro.props;
 ---

 ...
       }
     </style>
+    {!showToc && (
+      <style is:global>
+        .toc { display: none; }
+      </style>
+    )}
   </head>

test.md 以外のページでは目次が非表示になりました。

目次の表示/非表示を切り替えられるようにしたときのプレビュー画像

見出しにリンクを追加する

さらに、目次とは直接関係ないですが、rehype-slug で見出しにジャンプできるようになったので、下記のような見出しへのリンクも取れると便利でしょう。

GitHub上で見出しについているリンク

これは rehype-autolink-headings を使うと実現できます。インストールして astro.config.mjs に設定を追加します。

npm i rehype-autolink-headings
  ...
  markdown: {
    rehypePlugins: [
      "rehype-slug",
+      [
+        "rehype-autolink-headings",
+        {
+          behavior: "append",
+          content: {
+            type: "element",
+            tagName: "i",
+            properties: {
+              className: ["heading-anchor", "fa", "fa-link"],
+            },
+            children: [],
+          },
+        },
+      ],
+      ["rehype-toc", { headings: ["h2", "h3"] }],
+    ],
      ["rehype-toc", { headings: ["h2", "h3"] }],
    ],
  },
 });

今回は簡単のために、 behavior="append" を指定して heading 要素内の末尾にリンクを生成するようにします。また、Font Awesome のアイコンがリンク内に生成されるよう、contenthast で記述します。

src/components/BaseHead.astro 内で Font Awesome の CSS を読み込みます。

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css" />

すると生成される h2 が次のようになり、見出し内部に a タグとアイコンが挿入されているのがわかります。

<h2 id="introduction">
  Introduction
  <a aria-hidden="true" tabindex="-1" href="#introduction">
    <i class="heading-anchor fa fa-link"></i>
  </a>
</h2>

最後に src/styles/global.css で、見出しにカーソルを合わせたらアイコンが表示されるようにしてみます。

...
+ .heading-anchor {
+   visibility:hidden;
+   font-size: 1rem;
+   margin-left: 0.3rem;
+ }
+ 
+ h2:hover .heading-anchor {
+   visibility:visible;
+ }
+ h3:hover .heading-anchor {
+   visibility:visible;
+ }
記事の見出しにリンクを付けたときの挙動

簡易的ですが、見出しからもリンクを拾えるようになったのでコピーしやすくなりました!

さいごに

Astro のブログに目次を作成する方法を紹介しました。rehype のプラグインを使うことで簡単に実装できることがわかりました。remark のプラグインとも連携できるので、他にも色々なことに挑戦できそうです。

今回作成したコードは raahii/astro-toc-example に置いていますので参考にしてください。

参考