開発完了
PlaywrightでVueコンポーネントをOGP画像に変換する
Vueコンポーネントで作成したUIを、そのままOGP画像として出力する方法を解説する。Cloudflare Workersでの動的生成ではなく、Playwrightでスクリーンショットを撮影して静的画像として配信するアプローチを採用した。
結論
Playwrightで開発サーバー上のプレビューページをスクリーンショットし、静的OGP画像として保存する方式が最適だった。
- ビルド時間に影響しない(手動実行のスクリプト)
- Vueコンポーネントの見た目をそのままOGPに反映できる
- 更新が必要な時だけスクリプトを実行すればよい

背景:OG Workerの限界
当初、Cloudflare WorkersでOGP画像を動的に生成していた。Satori + Resvgを使ってSVGを描画し、PNGに変換する方式だ。
しかし、デザイン原則ページのように複雑なUIをOGPにしたい場合、Workerで一から描画するのは現実的ではなかった。
- Vueコンポーネントで既に良いUIがある
- それをWorkerで再実装するのは二度手間
- Satoriの制約(対応CSSが限られる)もある
解決策:Playwrightでスクリーンショット
アーキテクチャ
[Vueコンポーネント]
↓
[OGプレビューページ] ← 1200x630固定サイズでレンダリング
↓
[Playwright] ← スクリーンショット撮影
↓
[静的PNG画像] → public/og/pages/design-principles/{category}/{no}.png
OGプレビューページの作成
OGP用のプレビューページを pages/og-preview/design-principles/[category]/[no].vue に作成した。
<template>
<div class="og-container" id="og-capture">
<!-- ヘッダー -->
<div class="og-header">
<span class="og-breadcrumb">デザイン原則 / {{ categoryName }}</span>
</div>
<!-- コンポーネント -->
<div class="og-content">
<component :is="PrincipleComponent" v-if="PrincipleComponent" />
</div>
<!-- フッター -->
<div class="og-footer">
<span class="og-url">log.eurekapu.com</span>
</div>
</div>
</template>
<style scoped>
.og-container {
width: 1200px;
height: 630px;
/* ... */
}
</style>
ポイント:
#og-captureをPlaywrightのセレクタとして使用- 1200x630ピクセルに固定(OGP標準サイズ)
layout: falseでNuxtのレイアウトを無効化
Playwrightスクリプト
scripts/generate-design-principles-og.ts でスクリーンショットを撮影する。
import { chromium } from 'playwright'
const browser = await chromium.launch({ headless: true })
const context = await browser.newContext({
viewport: { width: 1200, height: 800 },
deviceScaleFactor: 2, // 高解像度
})
const page = await context.newPage()
await page.goto(url, { waitUntil: 'networkidle' })
// スタイルシートの読み込みを待機(重要)
await page.waitForFunction(() => {
const sheets = document.styleSheets
for (let i = 0; i < sheets.length; i++) {
try { sheets[i].cssRules } catch (e) { return false }
}
return true
}, { timeout: 10000 })
const container = await page.waitForSelector('#og-capture')
await container.screenshot({ path: outputPath, type: 'png' })
注意点: waitForFunction でスタイルシートの読み込みを待つのが重要。これがないと、CSSが適用される前にスクリーンショットが撮られてしまう。
実行方法
# 開発サーバーを起動
pnpm dev
# 別ターミナルでスクリプト実行
npx tsx scripts/generate-design-principles-og.ts
静的画像の参照
生成した画像はNuxtページから参照する。
// pages/design-principles/[category]/[no].vue
const ogImageUrl = computed(() => {
return `https://log.eurekapu.com/og/pages/design-principles/${category}/${no}.png`
})
useSeoMeta({
ogImage: ogImageUrl.value,
})
OG Workerの削除
静的画像に移行したため、OG Workerのdesign-principlesハンドラーは削除した。
// apps/workers/og/src/index.ts
// 以下のルートを削除
// if (url.pathname.startsWith('/og/pages/design-principles/')) {
// return handleDesignPrinciplesOgImage(url, env)
// }
まとめ
| 項目 | OG Worker(動的) | Playwright(静的) |
|---|---|---|
| 実装コスト | 高(UIを再実装) | 低(既存コンポーネント流用) |
| 更新方法 | 自動 | 手動スクリプト |
| ビルド時間 | 影響なし | 影響なし |
| 柔軟性 | Satoriの制約あり | Vueの全機能が使える |
Vueコンポーネントで既にUIを持っている場合、Playwrightでスクリーンショットを撮る方式が効率的だ。ビルド時間への影響もなく、必要な時だけ手動で更新できる。