ブログのサムネSVGをOGP用PNGに変換しようとしたら、ブラウザ自動化ツールがGoogle Fontsを読み込めず、日本語見出しが崩れて描画された。原因は「このファイルは外部か」というサンドボックスの判定にあった、という一次情報の記録。
サンドボックス化されたブラウザ自動化ツールが「どこまでを外部ファイルとみなすか」の具体的な詰まりポイントと、詰まったときに使えるPlaywrightヘッドレスを自前で直接叩くという代替経路を、実際に踏んだ手順のまま共有する。
このブログ(FLOW)では記事ごとにthumb.svgというバナー型のサムネを用意している。一覧ページではSVGをそのまま埋め込めばWebフォントも効くが、X(旧Twitter)やSlackのカード表示(OGP)はSVGを描画してくれない。だから1200×630のSVGを、同じ見た目のPNGに焼き直すogp.pngが別途必要になる。
やることは単純で、SVGを表示したHTMLをブラウザで開いてスクリーンショットを撮ればいい。ブラウザ自動化ツール(本サイトの執筆環境ではAIエージェント経由のブラウザプレビュー)で、そのSVGを埋め込んだ一時HTMLを開いてみた。
結果は、日本語の見出しがゴシック体ではなくOSの代替フォントで描画され、文字間隔も崩れていた。CSSでは確かにZen Kaku Gothic NewをGoogle Fontsから読み込む指定をしている。指定は効いていない。
エラーメッセージと挙動を辿って分かった、詰まりポイントの実体。
ブラウザプレビューツールは、開いたページが「プロジェクトの一部」か「ただのファイル」かを判定し、後者だと静的スナップショット扱いにしてContent-Security-Policyをdefault-src 'none'相当まで絞り込む。この状態ではGoogle FontsへのCSSリクエストそのものがブロックされる。
「じゃあプロジェクト内に一時HTMLを置けば通るはず」と考え、実際に投稿予約キュー(Blog/queue/配下)にHTMLを置いて開き直した。結果は変わらず、同じ"outside the project folder"という応答が返ってきた。ツールが見ている「プロジェクトの境界」は、こちらが想定する作業ディレクトリの境界と一致していなかった。
Google Fontsが読めないなら、と@font-faceでローカルフォントを埋め込む案も検討したが、これも外部リソース読み込みという扱いは変わらず、同じCSPの壁に当たることが分かった時点で「このツールの中で直す」方針を諦めた。
見えている制約と、実際に効いている制約は別物。「プロジェクト内に置けば通る」という手元の仮説は、ツール側の判定基準とは無関係だった。仮説が外れたら、同じツールの中で粘るより先に「そもそもこのツールで解決すべき仕事か」を疑う方が早い。
AIエージェントに用意されたブラウザプレビューは、あくまで人間が見るプレビュー用に設計されたサンドボックスであり、CSPの緩和は用途の外にある。そこで、プレビューツールではなくPythonから Playwright のheadless Chromiumを直接起動する経路に切り替えた。
実行環境にはplaywright==1.61.1がすでにグローバルに導入済みで、追加インストールは不要だった。手順は次の3ステップだけ。
# thumb.svg をそのまま読み込むだけの最小HTMLを用意し、
# headless Chromiumで開いてスクリーンショットを撮る
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1200, "height": 630})
page.goto(f"file://{html_path}")
page.wait_for_timeout(500) # Webフォント読み込み待ち
page.screenshot(path="ogp.png")
browser.close()
プレビューツールが「外部か内部か」を判定していたのに対し、Playwrightを自前で起動する経路にはそもそもその判定が存在しない。ローカルのfile://ページであっても、Chromiumが通常通りネットワークからGoogle Fontsを取得しに行き、wait_for_timeoutでフォント読み込みの完了を待ってから撮影すれば、一覧ページと同じ見た目のPNGがそのまま得られた。
ブログ・LP・OGP画像のように「見た目を画像として固定する」作業をAIエージェントに自動化させている場合、同じ壁に当たる可能性がある。次に同じ症状に出会ったときのために、判断のポイントだけ残しておく。