pre-commit+playwrightでUIプレビューを自動生成し、READMEを常に最新に保つ仕組み
UIを触るたびにREADMEのスクリーンショットが古くなる問題は、地味にストレスが溜まる。
毎回手動でキャプチャして差し替えるのも面倒だし、やらなくなるのが目に見えている。
そこで、pre-commitをフックにしてUIプレビューを自動生成し、そのままREADMEに反映させる仕組みを組んだ。
一度作ってしまえば、以降は「いつの間にかREADMEが最新になっている」状態になる。
例えば、こんな感じのUIプレビューがコミットごとに自動更新される。

全体の流れ
やっていることはシンプルで、処理の流れはこうなる。
- コードやUIを編集する
- pre-commitでローカル開発サーバーを起動し、Playwrightでスクリーンショットを撮る
-
[Optional]生成されたPNGをBase64でSVGに埋め込む(GitHub表示対策) - README.mdはそのSVG(またはPNG)を参照するだけ
ポイントは 「pre-commitで必ず実行されるが、無駄な再生成はしない」 ところ。
[Optional] なぜSVGに変換するのか
最初は単純にPNGをREADMEに貼っていたが、GitHubではCSSや影、背景の表現が効かない。
UIの雰囲気が伝わりにくく、せっかくのプレビューがのっぺりして見える。
そこで、PNGを Base64でSVGに埋め込み、枠やシャドウをSVG側で表現 する方式に切り替えた。
これならGitHub上でも見た目をコントロールできる。
pre-commit構成
.pre-commit-config.yaml ではローカルフックを2つ定義している。
- UIプレビュー生成
- PNG → SVGカード生成
- repo: local
hooks:
- id: make-preview
name: make preview with PREVIEW_OUTPUT
language: system
entry: ./scripts/maybe_make_preview.sh
pass_filenames: false
- id: generate-web-editor-card
name: generate web editor card svg
language: system
entry: python3 scripts/generate_web_editor_card.py
pass_filenames: false
ここでは ファイル差分はpre-commitに任せず、スクリプト側で制御 している。
.pre-commit-config.yaml 全体
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: local
hooks:
- id: make-preview
name: make preview with PREVIEW_OUTPUT
language: system
entry: ./scripts/maybe_make_preview.sh
pass_filenames: false
- id: generate-web-editor-card
name: generate web editor card svg
language: system
entry: python3 scripts/generate_web_editor_card.py
pass_filenames: false
無限ループを避けるための工夫
pre-commitで生成物を書き換えると、毎回差分が出てコミットできなくなる。
この問題を避けるために、影響しうるファイル群のハッシュを取る方式を採用した。
scripts/maybe_make_preview.sh の役割は以下。
- UIに影響するファイルだけを
git ls-filesで列挙 - それらのSHA-256をまとめてハッシュ化
- 前回と同じならプレビュー生成をスキップ
- 変わっていれば生成し、ハッシュを
.preview.hashに保存
CURRENT_HASH=$(printf '%s\0' "${PREVIEW_FILES[@]}" | xargs -0 sha256sum | sha256sum | cut -d ' ' -f1)
if [[ -f "$HASH_FILE" ]] && [[ "$(cat "$HASH_FILE")" == "$CURRENT_HASH" ]]; then
echo "preview: no relevant changes detected, skipping"
exit 0
fi
この方式にしてから、pre-commitが「賢くなった」感覚がある。
不要な起動やキャプチャが走らず、体感もかなり軽い。
プレビュー生成の中身
実際のスクリーンショット生成はPythonとPlaywrightに任せている。
- Pythonでローカル開発サーバーを起動
- URLが立ち上がるまでポーリング
- NodeスクリプトでPlaywrightを起動しキャプチャ
- 終わったらサーバーを確実に終了
preview.py はこのオーケストレーション役だ。
if not wait_for_server(PREVIEW_URL):
print("Server did not become ready within the timeout window.")
return 1
subprocess.run(
["node", "scripts/capture.mjs", str(OUTPUT_PATH), PREVIEW_URL],
check=True,
)
Playwright側は極力シンプルにしている。
viewport指定と networkidle 待ちだけで、余計なことはしない。
SVGカード生成
最後に、生成されたPNGをSVGに包む。
scripts/generate_web_editor_card.py では、
- PNGをBase64エンコード
- SVGテンプレートに埋め込み
- 影・角丸・背景をSVG側で定義
という処理をしている。
この段階で「READMEに載せる前提の見た目」を完成させておくのがポイント。
README側はただ <img> で参照するだけになる。
実際に使ってみて
一度この仕組みを入れると、
- UIを触る
- そのまま
git commit - READMEのプレビューが勝手に更新されている
という状態になる。
「ドキュメントは後で直す」が起きにくくなるのが一番の収穫だった。
pre-commitは整形ツールという印象が強いが、ローカルで完結する自動化にはかなり向いている。
UIを持つリポジトリなら、わりと汎用的に使える構成だと思う。
- コード(Gist): https://gist.github.com/ackkerman/d60a849f7e3a0a63f983e4aa2d561f1e
- ファイル構成例
.pre-commit-config.yaml .Makefile .scripts/ ├── capture.mjs ├── generate_web_editor_card.py ├── maybe_make_preview.sh └── preview.py
- ファイル構成例
Discussion