---
title: Markdown をその場で試せる /preview を付けた
description: ビルドもデプロイもせず、ブラウザーで書いた Markdown がこのサイトだとどう見えるかを確かめられるようにした。記事と同じ renderer を wasm にしてブラウザーで動かしている。ファイルから読み込めるのは、スマホのクリップボードの制限を越えるため。
prev: 2026-10-25-permalink.md
---

ヘッダー右の `preview` リンクから `/preview` に行ける。textarea に書いた Markdown が、すぐ右にこのサイトの記事と同じ形で描かれる。保存はしない（[issue #71](https://github.com/podhmo/html-sandbox/issues/71)）。この記事自体を貼り付ければ、見出しのアンカーもアラートも表もそのまま出る。

## 欲しかった理由

tinymarkdown とブログの renderer がどこまで出来ているかを、ビルドやデプロイをせずに確認したかった。issue を立てたのはスマホからだった。LLM に生成させた Markdown を仮当てして、どういう見え方になるか試す用途が最初から頭にあった。スマホだとクリップボードの制限で長文を貼るのが難しいので、ファイル入力で `.md` をそのまま読み込めるようにもしてある。

## 描画はサイト自身にやらせた

ロマンを目指して、記事を描くのと同じ renderer をブラウザーに持っていった。`site` パッケージが記事用に持っているレンダリング（front matter からタイトルを取る、見出しに id と permalink、alert、```svg、コピーボタン）を丸ごと呼べる関数 `render(src, files)` として export して、`wasm-gc` で `preview.wasm` にした（`use-js-builtin-string`、[`experiments/browser-render`](https://github.com/podhmo/html-sandbox/tree/main/moonbit/experiments/browser-render) の spike と同じ載せ方）。

最初は Wasm を instantiate できなかったとき用の JS ビルド（`.mjs`）も作っていたのだけど、Wasm の動作確認が取れたので丸ごと捨てた。「ロマンより動く機能」の免罪符で残すより、1 経路だけを面倒見る方が良い。

## リンクは `posts/preview.md` を書いているつもりで解決

プレビュー内の相対リンクは、「あなたが `posts/preview.md` という記事を書いている」つもりで解決する。`../TODO.md` ならこのサイトの `/docs/` のページへ、存在しないパスは GitHub の blob URL へ飛ぶ。飛び先を検証するため、ページにはサイトが持っているファイル一覧を仕込んである。

## 副産物と落とし穴

- `code-copy.js` はページロード時だけ `pre` を飾っていたので、プレビューで挿入される `pre` にコピーボタンが付かなかった。`MutationObserver` で後から足されたノードも飾るように直した。
- Devin Review が「ファイル名に `</script>` があれば `<script type="text/plain">` を抜けて HTML を差し込める」と指摘した。ファイル一覧は `<script>` の生テキストに置くのをやめて、属性として書けば必ずエスケープされる `data-wasm` / `data-files` に移した。
- ```svg ブロックは生 HTML を通す仕様なので、他人が用意した Markdown を貼ると被害者のブラウザーでスクリプトが動きうる。記事と同じ見え方が目的なのでここは変えず、サニタイズは Future work に記録した。

## 確認

いつも通り `make check` と `make compare`（Go サーバーと Workers 用 wasm のバイト一致、今回から `/preview` も比較対象）。最後に実際の Chrome で Wasm が instantiate されるところまで見た。ページのファイル入力の横には小さく `rendered by wasm` と出るので、どの経路で動いているかも分かる。
