怠惰な開発者のためのリッチリンク

ブログのリンクを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分かかりました。今では、すべての投稿が自動的にソーシャルプレビュー画像を取得します。投稿ごとの手動作業はありません。自動化は数件の投稿の後で元が取れます。

動作するバージョンを出荷し、ギャップを発見し、反復します。


関連投稿: