4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gitを使わない人にも設計書を届けたい — Obsidian の Markdown を GAS Web App で社内公開する

4
Last updated at Posted at 2026-06-07

はじめに

設計書を書いた後、「これ、どうやって楽に渡そう」で毎回詰まる。
書くこと自体は別にいい。共有が一番のボトルネックだった。

自分の場合は Obsidian に設計書を書いている。ER 図もフロー図も Mermaid で書いて、リンクで章をつなげて、書き心地としてはかなり気に入っている。
ただ、これを 非エンジニアのメンバーに見せたい となった瞬間に詰まる。

  • Markdown 生ファイルを渡しても読んでもらえない(Mermaid がただのコードブロックに見える)
  • GitHub に上げる手もあるが、相手は git のアカウントすら持っていない
  • Notion にコピペすると Mermaid が毎回崩れる。画像化で逃げてもみたが、拡大した瞬間にボケて読めない
  • Obsidian Publish は月課金で、社内 4〜5 人に見せたいだけだと割に合わない

要するに、git を使っていないメンバーにも、ER 図つきの設計書を、URL ひとつでサクッと見せたい
これだけのことなのに、ちょうどいい手段が無かった。

最終的に GAS Web App に行き着いたので、その経緯と、Mermaid をベクターのままズームさせるところのハマりを残しておく。


選択肢を比較したメモ

採用前に検討したやつ。

手段 良いところ やめた理由
Obsidian Publish 公式・記法ほぼそのまま 月課金、社外公開前提でアクセス制御がゆるい
GitHub Pages + 静的サイトジェネレータ 自由度高い、無料 非エンジニアに git アカウントを取らせたくない、ビルド運用がだるい
Notion にコピペ 共有先がすでに Notion を使っているなら最強 Mermaid 崩れる、リンク貼り直しが地獄
GAS Web App Workspace 内限定で公開できる、URL 配るだけ、無料

Workspace を使っている前提なら、「Web App 公開で Anyone within <組織名>」を選ぶだけでアクセス制御が完結する のがデカい。Google アカウントは全員持っているので、相手側の準備がゼロで済む。


仕組みはシンプル

やってることは3段だけ。

  1. Obsidian の .md を取得(自分はローカルから clasp push する流れに乗せた)
  2. GAS 側で Markdown → HTML に変換(marked 系のライブラリを HtmlService に載せる)
  3. Mermaid のコードブロックは <pre class="mermaid"> に置き換えて、描画はブラウザ側の mermaid.js に任せる

doGet でこの HTML を返す。Web App としてデプロイ。実行ユーザーは「自分」、アクセスは「組織内のみ」。
これで https://script.google.com/a/macros/<domain>/s/<id>/exec が配れる URL になる。

URL を Slack に貼って終わり。
非エンジニアからすると 「リンクをクリックしたら設計書が見える Web ページ」 という認識で、Obsidian も git も意識しなくていい。


ハマったところ: Mermaid をベクターのままズームさせる

ここが一番時間を食った。

最初は素直に mermaid.initialize を叩いて描画させた。表示はされる。でも ER 図はテーブルが 10 個以上あると、画面に収まらない or 文字が読めないサイズで縮む。

「じゃあクリックしたら拡大画面にしよう」と考えて、最初にやったのが SVG を PNG 化して <img> を別タブで開く やつ。
やってみたら、拡大したときにピクセルがボケる。ER 図のリレーション線がジャギジャギになる。設計書として読めるレベルじゃない。

Mermaid はそもそも SVG で描画しているのに、わざわざラスタライズして使ってるのが間違いだった。

最終的にやったのは:

  • クリックで新タブを開く
  • 新タブには 元の SVG をそのまま埋め込む(PNG 化しない)
  • SVG にホイールズーム / ドラッグパンのハンドラを当てる(svg-pan-zoom あたりのライブラリを使うのが楽。自前で viewBox をいじる手もある)

これで 何倍に拡大しても線がシャープなまま で、ER 図のカラム名まで普通に読めるようになった。
ベクターのまま扱う、というだけのことなんだけど、最初の発想で「画像にしてから拡大」をやってしまうとここで詰む。

「Mermaid = SVG で描かれている」という当たり前を、いったん見失っていたなと思う。


やってみて思ったこと

地味だけど、効果がデカかったのは 「設計書を書く心理的ハードルが下がった」 ところ。

これまでは「どうせ見られないし」「貼り直しが面倒だし」が頭にあって、設計書を Obsidian の中だけに閉じ込めがちだった。
URL を配るだけで非エンジニアと同じ画面を見られるようになると、書いた瞬間に共有できる ので、書く動機が一段増える。

あと、レビュー会で ER 図を画面共有しながら「この外部キーって本当に要る?」みたいな議論が、非エンジニア込みでできるようになったのも大きい。
PNG だと拡大した瞬間に「読めません」で止まっていた会話が、ベクターだと普通に進む。


おまけ: 実装コード(一番大変だったところ)

ここが一番ハマったので、抜粋を貼っておく。
構成は GAS 側の doGet と、ブラウザに返す HTML テンプレートの 2 ファイル。

1. GAS 側 (main.gs)

function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('設計書')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// HTMLテンプレート内から別ファイルを取り込むヘルパー
// 使い方: <?!= include('md_content') ?>
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

Markdown 本体は md_content.html のような別ファイルに切り出して、<script id="md-source" type="text/plain">…本文…</script> の形で埋め込んでおく。
これで本文の差し替えが md_content.html の上書きだけで済む。

2. HTML テンプレート (index.html)

marked.js と mermaid.js を CDN から読み込み、<pre><code class="language-mermaid"><div class="mermaid"> に置換してから mermaid.run() を叩く。描画が終わったタイミングでクリックハンドラを当てる。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
  <style>
    .mermaid {
      cursor: pointer;
      position: relative;
      padding: 16px;
      background: #fafafa;
      border-radius: 5px;
    }
    .mermaid::after {
      content: '🔍 クリックで新タブ表示(ズーム可)';
      position: absolute;
      top: 8px; right: 12px;
      font-size: 12px; color: #888;
      pointer-events: none;
    }
    /* 見出し・テーブル等のスタイルは省略 */
  </style>
</head>
<body>
  <div id="content"></div>
  <?!= include('md_content') ?>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // Markdown → HTML
      const mdText = document.getElementById('md-source').textContent;
      marked.setOptions({ breaks: false, gfm: true });
      document.getElementById('content').innerHTML = marked.parse(mdText);

      // ```mermaid コードブロックを <div class="mermaid"> に変換
      document.querySelectorAll('pre code.language-mermaid').forEach(function(el) {
        const div = document.createElement('div');
        div.className = 'mermaid';
        div.textContent = el.textContent;
        el.parentElement.replaceWith(div);
      });

      // 描画 → 描画完了後にクリックハンドラを当てる
      mermaid.initialize({ startOnLoad: false, theme: 'default' });
      mermaid.run().then(attachClickToNewTab);
    });
  </script>
</body>
</html>

3. 肝になる部分: クリック→SVG をそのまま新タブで開く

PNG 化せず、Mermaid が出力した SVG をそのまま Blob に詰めて新タブに渡す。
新タブ側では viewBox をホイール量に応じて書き換える。それだけで、ライブラリなしでズーム & パンが動く。

function attachClickToNewTab() {
  document.querySelectorAll('.mermaid').forEach(function(el) {
    el.addEventListener('click', function() {
      const svg = el.querySelector('svg');
      if (!svg) return;

      const svgString = new XMLSerializer().serializeToString(svg);

      // 新タブ用の独立 HTML を組み立て(インラインJSで viewBox を操作)
      // 注意: 文字列内で </script> をそのまま書くと外側のスクリプトが閉じてしまうので <\/script> にエスケープ
      const htmlDoc =
        '<!DOCTYPE html><html><head><meta charset="utf-8">'
        + '<style>'
        + 'html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#fafafa;}'
        + '#vp{position:fixed;inset:0;overflow:hidden;cursor:grab;}'
        + '#vp.dragging{cursor:grabbing;}'
        + '#vp svg{width:100%;height:100%;display:block;}'
        + '.hint{position:fixed;top:10px;left:50%;transform:translateX(-50%);'
        + 'background:rgba(0,0,0,0.7);color:#fff;padding:6px 14px;'
        + 'border-radius:4px;font-size:12px;pointer-events:none;}'
        + '</style></head><body>'
        + '<div class="hint">ホイール: ズーム / ドラッグ: 移動 / ダブルクリック: リセット</div>'
        + '<div id="vp">' + svgString + '</div>'
        + '<script>'
        + '(function(){'
        + 'var vp=document.getElementById("vp");'
        + 'var svg=vp.querySelector("svg");'
        // Mermaid が SVG に付ける固定 width/height を剥がして全画面に伸ばす
        + 'svg.removeAttribute("width");svg.removeAttribute("height");svg.removeAttribute("style");'
        + 'svg.setAttribute("preserveAspectRatio","xMidYMid meet");'
        // viewBox が無ければ getBBox で初期化
        + 'var vb=svg.viewBox.baseVal;'
        + 'if(!vb||vb.width===0){var bb=svg.getBBox();'
        + 'svg.setAttribute("viewBox",bb.x+" "+bb.y+" "+bb.width+" "+bb.height);'
        + 'vb={x:bb.x,y:bb.y,width:bb.width,height:bb.height};}'
        + 'var initX=vb.x,initY=vb.y,initW=vb.width,initH=vb.height;'
        + 'var x=initX,y=initY,w
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?