---
title: "Astro の Mermaid を rehype-mermaid から動的 import に切り替えた"
description: "AstroプロジェクトでMermaidダイアグラムを表示する方法を解説します。rehype-mermaidによるビルド時変換ではなくクライアントサイドレンダリングを選んだ理由、動的インポートでライブラリをページ単位で読み込む設計を紹介します。"
category: "Web"
tags: ["Astro","Mermaid","React","ダイアグラム","Markdown"]
publishedAt: "2026-04-20"
lastmod: "2026-04-20"
---

[Astro](https://astro.build/)で[Mermaid](https://mermaid.js.org/)ダイアグラムを表示するにあたり、クライアントサイドレンダリングを採用しました。[rehype-mermaid](https://github.com/remcohaszing/rehype-mermaid)のデフォルト戦略（`inline-svg`）では[Playwright](https://playwright.dev/)でヘッドレスブラウザーを起動してSVGを生成します。Playwright不要の `pre-mermaid` 戦略もありますが、これはクライアントサイドレンダリングに委ねる方式で、ビルド時にSVGを静的出力する目的には使えません。CI環境の複雑化を避けたかったことが最大の理由です。加えて、Mermaidを使うページが全体のごく一部であることから、該当ページでだけライブラリを動的インポートする方式が最もシンプルだと判断しました。

Labee Dev Toolboxの用語集では、SPFの認証フローやDNSルックアップの流れをMermaidのシーケンス図やフローチャートで説明しています。Astroには公式のMermaid対応がないため、表示方法を自前で実装しています。

::card[https://labee.dev/jp/glossary/spf]

## ビルド時変換を選ばなかった理由

MermaidのAstro対応としてまず検討したのは、`rehype-mermaid` のようなビルド時変換プラグインです。このプラグインはMarkdownのコードブロックをビルド時にSVGへ変換し、静的HTMLとして出力します。

`rehype-mermaid` の内部では、MermaidのJavaScript APIを実行してSVGを生成する必要があります。MermaidはブラウザーのDOM APIに依存しているため、Node.js上でそのまま実行することはできません。そこで `rehype-mermaid` はPlaywrightを使ってヘッドレスブラウザーを起動し、その中でMermaidのレンダリングを行います。具体的には、ヘッドレスChromiumのページ上でMermaidコードを渡し、レンダリング結果のSVGを取得してMarkdownの出力に埋め込むという流れです。

この方式を採用しなかった理由は2つあります。

1つ目は、ビルド時間です。ヘッドレスブラウザーを動かすにはChromiumのバイナリが必要で、ビルドのたびにPlaywrightのセットアップとブラウザー起動のコストがかかります。Mermaidを使うページが一部しかないのに、全ビルドでこのコストを払うのは割に合いません。

2つ目は、バンドルサイズの制御です。Mermaidのコアチャンクだけで約**600KB**、ダイアグラム関連を含めると数MBになります。静的インポートにするとメインのJSバンドルに含まれ、Mermaidを使わないページでも読み込まれてしまいます。動的インポートで該当ページだけに限定することで、不要なページへの影響をゼロにしています。

## [Shiki](https://shiki.style/)のシンタックスハイライト除外

最初の準備として、Mermaidのコードブロックをシンタックスハイライトの対象から外します。

```typescript
// astro.config.mjs
export default defineConfig({
  markdown: {
    syntaxHighlight: {
      type: 'shiki',
      excludeLangs: ['mermaid', 'math'],
    },
  },
})
```

この設定がないと、Shikiが ```` ```mermaid ```` ブロックをコードとしてハイライトし、`<span>` タグで装飾されたHTMLが出力されます。Mermaidライブラリは素のテキストを受け取る必要があるため、ハイライトを除外して `<code class="language-mermaid">` をそのまま出力させます。

[`excludeLangs`](https://docs.astro.build/en/reference/configuration-reference/#markdownsyntaxhighlightexcludelangs) のデフォルト値は `['math']` です。Astroは数式用の `math` 言語をデフォルトで除外しています。`mermaid` を追加する際にこのデフォルト値を上書きすることになるため、`math` を配列から落とすと[KaTeX](https://katex.org/)等の数式レンダリングが壊れます。明示的に `['mermaid', 'math']` と両方を指定する必要があります。

## MermaidRendererコンポーネント

[React](https://react.dev/)コンポーネントとして実装しました。Astroの `client:load` ディレクティブでハイドレーションします。

```typescript
// src/client/components/jp/MermaidRenderer.tsx
import { useEffect } from 'react'

export function MermaidRenderer() {
  useEffect(() => {
    const blocks = document.querySelectorAll('code.language-mermaid')
    if (blocks.length === 0) return

    let cancelled = false

    async function render() {
      const { default: mermaid } = await import('mermaid')
      if (cancelled) return

      mermaid.initialize({
        startOnLoad: false,
        theme: 'default',
        sequence: { width: 250, actorMargin: 80 },
      })

      const nodes: HTMLElement[] = []
      for (const code of blocks) {
        const pre = code.parentElement
        if (!(pre instanceof HTMLElement) || pre.tagName !== 'PRE') continue
        const source = code.textContent ?? ''
        pre.classList.add('mermaid')
        pre.textContent = source
        nodes.push(pre)
      }

      if (nodes.length > 0) {
        await mermaid.run({ nodes })
      }
    }

    render()
    return () => { cancelled = true }
  }, [])

  return null
}
```

`import('mermaid')` による動的インポートがこの設計の核です。Mermaidのコアチャンクだけで約**600KB**、ダイアグラム関連を含めると合計で数MBになります。静的インポートだとすべてのページのバンドルに含まれてしまいますが、動的インポートならMermaidブロックを含むページでのみダウンロードされます。

### `mermaid.initialize()` の各オプション

`startOnLoad: false` は、Mermaidのデフォルトの自動レンダリングを無効にする設定です。Mermaidは通常、ページ読み込み時に `.mermaid` クラスを持つ要素を自動検出してレンダリングします。しかしこの実装では、`<pre>` 要素のテキスト差し替えとクラス付与を手動で行ったうえで `mermaid.run()` を明示的に呼び出しています。**自動検出とタイミングが競合するのを避けるため**、`startOnLoad` を `false` にして手動制御に統一しています。

`theme: 'default'` は、Mermaidの組み込みテーマの中からデフォルトのカラーパレットを選択しています。`dark` や `forest` など他のテーマもありますが、サイトの配色と干渉しないデフォルトを選んでいます。

`sequence` オプションはシーケンス図専用のレイアウト設定です。`width: 250` はアクター（参加者）ボックスの幅をピクセル単位で指定しています。デフォルトでは**150px**ですが、日本語のラベルは英語より幅を取るため、余裕を持たせています。`actorMargin: 80` はアクター間の水平方向の余白です。ボックス幅を広げた分、余白も調整してダイアグラム全体のバランスを取っています。

### キャンセル処理

`cancelled` フラグはメモリリーク防止のためです。動的インポートは非同期処理なので、レンダリング完了前にページ遷移が起きた場合にDOM操作をスキップします。`useEffect` のクリーンアップ関数でフラグを `true` にし、`import()` の完了後にフラグを確認してから処理を続行します。

## ダイアグラムがあるページだけ読み込む

レイアウトコンポーネントで `hasMermaid` プロパティを受け取り、MermaidRendererの読み込みを制御します。

```astro
---
// src/layouts/JpContentLayout.astro
const { hasMermaid } = Astro.props
---

{hasMermaid && <MermaidRenderer client:load />}
```

ページ側では記事本文にMermaidブロックが含まれるかを正規表現でチェックします。

```astro
---
const hasMermaid = /```mermaid/.test(post.body ?? '')
---
```

この判定はビルド時に行われるため、Mermaidを使わないページのHTMLには**Reactコンポーネント自体が含まれません**。バンドルサイズへの影響はゼロです。

## CSSでコードブロックのスタイルをリセット

Mermaidが生成するSVGは、通常のコードブロックとは異なるスタイルが必要です。暗い背景色をリセットし、ダイアグラムを中央配置にします。

```css
.jp-content-body pre.mermaid {
  background: var(--jp-surface);
  padding: var(--jp-space-6);
  font-size: 1rem;
  text-align: center;
  overflow-x: auto;
}

.jp-content-body pre.mermaid svg {
  max-width: 100%;
  height: auto;
}
```

`overflow-x: auto` により、モバイルで横幅に収まらないダイアグラムはスクロールで確認できます。

## Markdownでの記述

記事の中ではコードブロックと同じ感覚で書けます。Labee Dev Toolboxの用語集では、SPFの認証フローをシーケンス図で、DNSルックアップの上限をフローチャートで説明しています。

````markdown
```mermaid
sequenceDiagram
    participant S as 送信サーバー
    participant R as 受信サーバー
    participant D as DNS

    S->>R: メール送信（MAIL FROM: user@example.com）
    R->>D: example.com TXT を問い合わせ
    D-->>R: SPF レコードを返却
    Note over R: 送信元 IP と許可リストを照合
```
````

ビルド後のページでは、コードブロックの代わりにSVGのシーケンス図が表示されます。

## テスト

テストでは、MermaidRendererの動作を3つの観点からカバーしています。

1つ目は、正常系のレンダリングです。`code.language-mermaid` を持つ要素がDOMに存在する状態でコンポーネントをマウントし、`mermaid.initialize()` と `mermaid.run()` が呼ばれることを確認します。渡される `nodes` 配列に対象の `<pre>` 要素が含まれていること、`<pre>` 要素に `mermaid` クラスが付与されていること、テキストコンテンツが元のMermaidソースコードに差し替わっていることも検証対象です。

2つ目は、Mermaidブロックが存在しないページでの振る舞いです。`code.language-mermaid` に該当する要素がない場合、`import('mermaid')` が呼ばれずに処理が早期リターンすることを確認します。不要なページでライブラリのダウンロードが発生しないことを保証するテストです。

3つ目は、アンマウント時のキャンセル処理です。コンポーネントがマウントされた直後にアンマウントし、動的インポートの完了後にDOM操作がスキップされることを確認します。ページ遷移時のメモリリークやエラーを防ぐための検証です。

数百KBのライブラリを使わないページで読み込まないこと、使うページでは確実にレンダリングすること。この2点がこの実装の判断基準でした。