はじめに
設計書を書いた後、「これ、どうやって楽に渡そう」で毎回詰まる。
書くこと自体は別にいい。共有が一番のボトルネックだった。
自分の場合は 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段だけ。
- Obsidian の
.mdを取得(自分はローカルから clasp push する流れに乗せた) - GAS 側で Markdown → HTML に変換(marked 系のライブラリを HtmlService に載せる)
- 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