← AIバイブコーディングTips 一覧へ Tooling / OGP画像自動生成

AIのブラウザツールがWebフォントを読まない時

ブログのサムネSVGをOGP用PNGに変換しようとしたら、ブラウザ自動化ツールがGoogle Fontsを読み込めず、日本語見出しが崩れて描画された。原因は「このファイルは外部か」というサンドボックスの判定にあった、という一次情報の記録。

この記事で手に入るもの

サンドボックス化されたブラウザ自動化ツールが「どこまでを外部ファイルとみなすか」の具体的な詰まりポイントと、詰まったときに使えるPlaywrightヘッドレスを自前で直接叩くという代替経路を、実際に踏んだ手順のまま共有する。

01 — 何が起きたか

SVGのサムネを、SNS共有用のPNGに変換したいだけだった

このブログ(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から読み込む指定をしている。指定は効いていない。


02 — 原因:CSPが「外部ファイル」を全部遮断していた

「プロジェクトの中か外か」の判定が、想定と違う場所で切られていた

エラーメッセージと挙動を辿って分かった、詰まりポイントの実体。

01

file:// は問答無用で「外部」扱い

ブラウザプレビューツールは、開いたページが「プロジェクトの一部」か「ただのファイル」かを判定し、後者だと静的スナップショット扱いにしてContent-Security-Policyをdefault-src 'none'相当まで絞り込む。この状態ではGoogle FontsへのCSSリクエストそのものがブロックされる。

02

プロジェクトフォルダの内側に置いても、同じ壁に当たった

「じゃあプロジェクト内に一時HTMLを置けば通るはず」と考え、実際に投稿予約キュー(Blog/queue/配下)にHTMLを置いて開き直した。結果は変わらず、同じ"outside the project folder"という応答が返ってきた。ツールが見ている「プロジェクトの境界」は、こちらが想定する作業ディレクトリの境界と一致していなかった。

03

CSSだけでなく、後から差し込むフォールバックも効かない

Google Fontsが読めないなら、と@font-faceでローカルフォントを埋め込む案も検討したが、これも外部リソース読み込みという扱いは変わらず、同じCSPの壁に当たることが分かった時点で「このツールの中で直す」方針を諦めた。


見えている制約と、実際に効いている制約は別物。「プロジェクト内に置けば通る」という手元の仮説は、ツール側の判定基準とは無関係だった。仮説が外れたら、同じツールの中で粘るより先に「そもそもこのツールで解決すべき仕事か」を疑う方が早い。

03 — 抜け道:Playwrightをヘッドレスで直接叩く

「ブラウザで見る」ツールを諦め、「ブラウザを動かす」コードに切り替えた

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がそのまま得られた。


04 — 教訓

詰まったツールの中で粘る前に、経路そのものを疑う

ブログ・LP・OGP画像のように「見た目を画像として固定する」作業をAIエージェントに自動化させている場合、同じ壁に当たる可能性がある。次に同じ症状に出会ったときのために、判断のポイントだけ残しておく。

症状日本語見出しがWebフォントで描画されず、OS標準フォントに落ちて文字間隔も崩れる。
疑うべき原因CSSの指定ミスより先に、開いている環境のCSP(Content-Security-Policy)が外部リソースの読み込みごと絞られていないかを疑う。
「プロジェクト内に置けば直る」を過信しないツールが判定する境界は、作業ディレクトリの境界と一致するとは限らない。同じ制限が再現するなら、それはCSSの問題ではなく環境の問題。
解決の型「見るためのプレビュー」で詰まったら、「動かすためのコード」に切り替える。Playwright/Puppeteerなどをスクリプトから直接起動すれば、プレビュー用サンドボックスのCSP制限を経由しない。