怠惰な開発者のためのリッチリンク
ブログのリンクをTwitterやSlackで共有すると、プレーンテキストとして表示されます。プレビュー画像はありません。granda.org/en/2026/01/02/claude-code-on-the-go/のようなURLだけです。
Open Graph画像が必要でした。標準的なアプローチ:各投稿に対して手動で1200x630の画像を作成する。それは面倒です。Claudeに自動化を依頼しました。
flowchart LR
Push[git push] --> GHA[GitHub Actions]
GHA --> Hugo[Hugo Server]
GHA --> PW[Playwright]
PW -->|screenshot| Hugo
PW --> IMG[OG Image]
IMG --> Commit[git commit]
Commit --> Deploy[Deploy]
flowchart TB
Push[git push] --> GHA[GitHub Actions]
GHA --> Hugo[Hugo Server]
GHA --> PW[Playwright]
PW -->|screenshot| Hugo
PW --> IMG[OG Image]
IMG --> Commit[git commit]
Commit --> Deploy[Deploy]
セットアップ
Claudeに問題を説明しました:投稿にはソーシャルプレビュー画像が必要ですが、手動で作成したくありません。記事の内容をスクリーンショットし、OG画像として保存し、フロントマターを自動的に更新します。
Claudeが構築したもの
| コンポーネント | 目的 |
|---|---|
generate-og-image.js | 投稿のスクリーンショットを撮るPlaywrightスクリプト |
generate-og-images.yml | 投稿の変更時にトリガーされるGitHub Action |
baseof.htmlの変更 | 条件付きog:imageメタタグ |
PlaywrightスクリプトはヘッドレスのChromeを起動し、投稿に移動し、ライトモードを強制し、ヘッダーとフッターを非表示にし、1200x630でスクリーンショットを撮ります:
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 2, // Retina for crisp text
});
// Force light mode, hide nav
await page.evaluate(() => {
document.documentElement.setAttribute("data-theme", "light");
document.querySelector("header").style.display = "none";
document.querySelector("footer").style.display = "none";
});
await page.screenshot({ path: outputPath, type: "png" });
GitHub Actionは.en.mdファイルの変更時にトリガーされ、Hugoを起動し、スクリプトを実行し、生成された画像をリポジトリにコミットします:
on:
push:
branches: [main]
paths:
- "content/posts/**/*.md"
steps:
- name: Start Hugo server
run: hugo server --bind 0.0.0.0 --port 1313 &
- name: Generate OG images
run: node scripts/generate-og-image.js "$file"
- name: Commit changes
run: |
git add static/images/posts/ content/posts/
git commit -m "Generate Open Graph images for blog posts"
git push
テンプレートは、フロントマターにimageフィールドがある場合に条件付きで画像を含めます:
{{- if .Params.image -}}
<meta property="og:image" content="{{ $canonical }}{{ .Params.image }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{{ $canonical }}{{ .Params.image }}">
{{- else -}}
<meta name="twitter:card" content="summary">
{{- end -}}
404: スラグが見つかりません
初回実行。Playwrightは空白のページをスクリーンショットしました。URLは404を返しました。
スクリプトはファイル名からURLを構築していました:automatic-blog-translations.en.md → /en/2025/12/23/automatic-blog-translations/
しかし、Hugoはスラグにファイル名を使用しません。タイトルを使用します。実際のURLは/en/2025/12/23/automatic-blog-translations-with-claude-and-github-actions/でした。
HugoのslugifyアルゴリズムをJavaScriptで再実装することもできました。代わりに:Hugoに尋ねます。
const output = execSync("hugo list all", { encoding: "utf8" });
// CSV output includes the exact permalink Hugo generates
1行。エッジケースなし。Hugoはすでに自身のURLを知っています。
次のステップ
現在の実装は英語の投稿のみに画像を生成します。翻訳されたバージョンは同じ画像パスを参照しており、それは機能しますが、スペイン語や日本語のバージョンを共有する場合でも、OG画像は常に英語のテキストを表示することを意味します。
真のi18nサポートは次のPRで提供されます。今のところ、すべての投稿がソーシャル画像を取得します。これは何もないよりも優れています。
まとめ
これは構築とデバッグに14分かかりました。今では、すべての投稿が自動的にソーシャルプレビュー画像を取得します。投稿ごとの手動作業はありません。自動化は数件の投稿の後で元が取れます。
動作するバージョンを出荷し、ギャップを発見し、反復します。
関連投稿:
- ClaudeとGitHub Actionsによる自動ブログ翻訳 - 同じスタック、異なる自動化
- 私のQAエンジニアはLLMです - Claudeに自分が書いたコードをレビューさせる
- Claudeで独自のニュースレターを作成 - もう1つの30分ビルド