本文へ移動

RailsでMermaid記法を動かすためのアーキテクチャ解説

RailsでMermaid記法を動かすためのアーキテクチャ解説の説明画像

「描く」から「生成する」へ:AIとMermaidの強力な親和性

これまで、フロー図やシーケンス図を作成するには、Draw.ioや各種ドローツールを開き、
マウスを使って時間をかけてボックスや矢印を配置するのが一般的だったように感じます。
しかし、生成AIが日常的なツールとなった今、この「手作業で描く」スタイルは大きな転換点を迎えています。

Mermaidは、テキストで図の構造を定義できます。この形式を使えば、AIに図のたたき台となるコードを生成させ、エンジニアが構文と内容を確認してからMarkdownへ組み込む、という使い方ができます。生成された図が実際の処理や関係を正しく表しているかは、元のコードや仕様と照らして確認する必要があります。

本記事では、RailsアプリケーションのブログにMermaid.jsを組み込み、
AIで作った図の定義を確認し、Markdownから図を生成するアーキテクチャ
について解説します。

アーキテクチャ全体像

単にライブラリを読み込むだけでなく、「サーバー側でのマークアップ」と「クライアント側での描画」の役割を明確に分離しました。

1. サーバー側:Markdownレンダラーの拡張

Markdownを解析する際、Mermaidのコードブロックを「ただのコード」としてではなく、「図の種」として出力します。

  • Gem選定: redcarpet を採用。高速かつ、レンダリング処理を自由にオーバーライドできるためです。
  • カスタムレンダラー: block_code メソッドを上書きし、言語指定が mermaid の場合のみ <div class="mermaid"> を出力するように変更。これにより、既存のシンタックスハイライト(Prism.js等)との干渉を防ぎます。
  • セキュリティ: html_escape は、図の定義をHTMLに埋め込む際の特殊文字をエスケープします。当サイトの実装では、同じ目的で CGI.escapeHTML を使用しています。これだけで、Mermaidによる描画後まで含めたXSSリスクを排除できるわけではありません。当サイトはHTMLラベルやクリックを許可する securityLevel: 'loose' を使っているため、図の入力を信頼できる内容に限定する必要があります。任意のユーザー入力を扱う場合は、既定の strict を含め、入力元に応じた設定を確認してください(Mermaid公式のsecurityLevel )。

2. クライアント側:ライフサイクルに合わせた描画制御

Rails特有の「画面遷移(Turbo/Turbolinks)」と「描画パフォーマンス」の両立がキモです。

  • 遅延ロード: 常にMermaidを読み込むのではなく、ページ内に .mermaid クラスが存在する時だけ CDN から動的にロード。
  • 二重描画の防止: data-rendered 属性を活用。Turboによるページ復元時や、動的なコンテンツ追加時に、「まだ図解されていないノードだけ」を狙って mermaid.run() を実行します。

3. プレゼンテーション層:レスポンシブ対応

MermaidのSVGは、放っておくと画面幅を突き抜けることがあります。
CSSで「枠」と「余白」を制御し、スマホ閲覧時は横スクロールを許容する設計にしました。

実装のコア・スニペット

サーバーサイド(Ruby)

パーサーを拡張し、特定のフェンス(```mermaid)を検知するロジックです。

# app/helpers/custom_markdown_renderer.rb
class CustomMarkdownRenderer < Redcarpet::Render::HTML
  def block_code(code, language)
    return '' if code.nil?
    lang = language.to_s.strip.downcase

    if lang == 'mermaid'
      # Mermaid.jsが解釈できる形式に変換
      %(<div class="mermaid">#{ERB::Util.html_escape(code)}</div>)
    else
      # 通常のコードハイライト用
      %(<pre><code class="language-#{ERB::Util.html_escape(lang)}">#{ERB::Util.html_escape(code)}</code></pre>)
    end
  end
end

クライアントサイド(JavaScript)

以下はMermaid 10系のES Modules版を使う、Turbo向けの簡略例です。初回の turbo:load より前に、一度だけ読み込むJavaScriptファイルへ置きます。Turbolinks 5の環境では、末尾を document.addEventListener("turbolinks:load", initMermaid); に置き換えてください。動的に図を追加した場合は、追加後に initMermaid() を呼ぶ必要があります。当サイトの実装では、script 要素による遅延ロードと turbolinks:load / DOMContentLoaded を組み合わせています(Mermaid公式のrun Turboのイベント Turbolinksのイベント )。

// Mermaidの初期化と実行
const initMermaid = async () => {
  const targets = document.querySelectorAll('.mermaid:not([data-rendered])');
  if (targets.length === 0) return;

  // 動的インポート(必要な時だけロード)
  const { default: mermaid } = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');

  mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
  await mermaid.run({ nodes: targets });

  // 描画済みフラグを立てる
  targets.forEach(n => n.dataset.rendered = 'true');
};

// Turboの遷移イベントに合わせて発火
document.addEventListener("turbo:load", initMermaid);

お披露目:シーケンス図サンプル

sequenceDiagram participant U as User participant R as Rails participant M as Mermaid U->>R: /modern_public_blog_articles/:id R-->>U: HTML + .mermaid U->>M: JSロード M-->>U: SVG生成

ブラウザ上では、中央寄せのSVGとして表示されます。コードブロックのコピー機能や他のハイライト表示は従来どおり維持できました。

まとめ

ちょうどブログにも図を使った説明を使っていきたいと思っていたところだったので、意外に簡単に導入できてよかったです!
ぜひ皆さんも表現の幅を広げるために導入してみてはいかがでしょうか?

この記事をシェア

管理人プロフィール

あゆの塩焼きのプロフィール画像

あゆの塩焼き

現役Webエンジニア。日々の開発で得た学びや思ったことを記録しています。

詳しいプロフィールを見る

次の学びも、見逃さずに。

AIの実例・研究・ブログ・読書の更新を、RSSリーダーでまとめて購読できます。

RSSを購読する

Loading...