ある日、自社の英語アカウントで「最新ブログ記事の告知」を X に投稿して、表示された OGP カードを見て凍りつきました。画像が、記事のものではなく、LP のセールスバナー “Stop babysitting updates. Start scaling maintenance revenue.” になっている。
慌てて過去 7 本の告知投稿を遡って確認したら、全部同じ LP セールス画像で表示されていました。エンジニア向けのテックブログとして書いていた記事が、X のタイムライン上では「セールス LP の宣伝投稿」として何ヶ月も流れ続けていたわけです。
今回はこの構造的事故をどう発見して、どう「動的 OGP 画像生成エンジン」で恒久的に解決したかを、自社ブログ自身を題材にまとめておきます。
構造的事故の正体 — 「アイキャッチ未設定 → LP デフォルト」分岐
掘ってみると、テーマの OGP ロジックは「アイキャッチ画像があればそれ、無ければ LP のデフォルト」というよくある fallback パターンでした。問題は、JP/EN 全 21 本 × 2 = 42 本のすべてでアイキャッチが未設定だったこと。全記事で同じ LP セールス画像が og:image に出ていたわけです。
補足: OGP(Open Graph Protocol)は、SNS で URL を共有した時に表示される「カード」のメタ情報。X / Facebook / LinkedIn / Slack など主要 SNS が共通仕様として参照します。
選択肢の検討 — 手動アイキャッチ vs 動的生成
修復の方向で 2 つの案がありました。
案 A. 全 21 本にアイキャッチ画像を手動設定する: 真っ当な対応です。が、(1) 21 本 × 2 言語 = 42 枚の画像を 1 回作るだけでも工数大、(2) 今後の新記事公開ごとに「アイキャッチ作成」が継続コストとして積み上がる、(3) 日本式の「PC やキーボードのストック写真」アイキャッチは、海外テック界隈では amateur シグナルとして受け取られやすい — という難点を抱えています。
案 B. 動的 OGP 画像生成エンジンを実装する: 海外テックブログの標準パターン。Vercel・PlanetScale・dev.to などが採用している「黒背景 + 記事タイトルを大文字で合成」型の OGP カードを、リクエスト時に動的にレンダリングします。初回生成・以降キャッシュ。記事公開のたびに自動で記事固有の OGP が生まれます。
採用したのは 案 B でした。理由は次の通りです。
- 1 回エンジンを作れば 21 本も将来の記事も自動で正しい OGP になる
- 「黒背景タイトル合成」は dev.to や Vercel ブログで読者が見慣れたプロトーン
- アイキャッチを「素材として用意する」発想から解放される
実装の核 — ogp-generator.php 1 ファイル(PHP GD)
採用したのは PHP GD(PHP に標準で入っている画像生成ライブラリ)で 1200×630 PNG を動的レンダリングする設計です。375 行の独立 PHP ファイル ogp-generator.php 1 つに集約しました。
// ogp-generator.php の核(概念図)
$post_id = (int)($_GET['post_id'] ?? 0);
$cache_path = WP_CONTENT_DIR . "/uploads/ogp/post-{$post_id}.png";
// 2 回目以降はキャッシュ静的配信
if (file_exists($cache_path)) {
header('Content-Type: image/png');
readfile($cache_path);
exit;
}
// 初回: WP DB から記事タイトル取得
$post = get_post($post_id);
$title = $post->post_title;
// PHP GD で 1200×630 PNG をレンダリング
$im = imagecreatetruecolor(1200, 630);
// 背景 #0d1117(LP のメインダーク)
$bg = imagecolorallocate($im, 0x0d, 0x11, 0x17);
imagefilledrectangle($im, 0, 0, 1200, 630, $bg);
// 上端 青 4px / 下端 緑 4px ライン(LP との連続性)
$blue = imagecolorallocate($im, 0x2f, 0x81, 0xf7);
$green = imagecolorallocate($im, 0x3f, 0xb9, 0x50);
imagefilledrectangle($im, 0, 0, 1200, 4, $blue);
imagefilledrectangle($im, 0, 626, 1200, 630, $green);
// タイトルレンダリング(言語別フォント選択)
$font = has_japanese($title) ? NOTO_BOLD : INTER_BOLD;
$lines = wrap_text($title, $font, 56, 900);
$y = 220;
$white = imagecolorallocate($im, 0xff, 0xff, 0xff);
foreach ($lines as $line) {
imagettftext($im, 56, 0, 100, $y, $white, $font, $line);
$y += 88;
}
// ディスクに保存して配信
imagepng($im, $cache_path);
header('Content-Type: image/png');
readfile($cache_path);
シンプルですが、必要な構成要素はすべて入っています。初回はレンダリング・以降はディスクキャッシュから静的配信で、リクエストごとに GD を回す重さは初回 1 度だけになります。
日本語タイトルのフォント — Inter と NotoSansJP の併用
最初の検証で躓いたのが「Inter フォントは日本語が一切出ない」問題です。Inter は欧文専用で JIS X 0208 範囲の文字を持っていません。imagettftext() に日本語タイトルと Inter を渡すと、豆腐(□□□)が並ぶか何も描画されない。タイトルに日本語が含まれるかを判定してフォントを切り替えます。
function has_japanese(string $text): bool {
return preg_match('/[ぁ-んァ-ヶ一-龯]/u', $text) === 1;
}
Google Fonts から Noto Sans JP Bold(SIL Open Font License・5.1 MB)を持ってきてテーマに同梱。Inter Bold / Regular(同じく SIL OFL・各 325 KB 前後)も併せて、テーマファイル群と一緒に rsync デプロイできます。
functions.php の分岐ロジック — +28 / -2
OGP ロジックを動的生成パスに差し替えます。修正の差分は実質 +28 / -2 行と小さく、既存挙動(アイキャッチが設定されている記事はそのまま)を温存しました。
// 修正後: アイキャッチ未設定の単一記事は動的 URL を返す
function wpmm_blog_og_image($post) {
if (is_singular() && !has_post_thumbnail($post)) {
$cache_bust = date('YmdHis');
return home_url(
"/wp-content/themes/wpmm-blog/ogp-generator.php" .
"?post_id={$post->ID}&v={$cache_bust}"
);
}
// アイキャッチがある記事はそれを優先
$thumb = get_the_post_thumbnail_url($post, 'full');
return $thumb ?: LP_DEFAULT_OGP;
}
&v=YYYYMMDDHHMMSS はキャッシュバスティング用です。テーマを更新してデザイン仕様を変えた時、X や Facebook がキャッシュした古い OGP 画像を再クロールさせる効きます。
デザインの設計判断と効果
サイズは OGP 標準の 1200×630(X large image card 推奨)。背景 #0d1117・アクセント青 #2f81f7 / 緑 #3fb950 は LP のブランドカラーから引いて、上端青 4px / 下端緑 4px の細ラインで LP との視覚的連続性を出します。左上に「WP MAINTENANCE MANAGER」+ 「BLOG」、中央左寄せに記事タイトル、右下に補助 URL。Vercel の og API や PlanetScale の OGP カードと近いトーンに揃えて、読者が「プロのテックブログだ」と認識する視覚要素を踏襲しています。
デプロイ後 curl -I で 42 本の og:image を一斉確認したところ、すべて動的 URL を返すようになっていました。X TL での見た目も、LP セールス画像から 黒背景 + 記事タイトル合成のテックカード に切り替わってトーンが揃っています。過去の告知投稿の遡及修正は時事性が切れているので実施せず、「LP 画像時代 vs 動的 OGP 時代」のエンゲージメント差を継続蓄積して今後の運用判断に使う方針です。
学び — 動的 OGP 設計の 4 つの原則
このラウンドから取り出せる原則は次の 4 つでした。
- WP テーマの OGP デフォルトは無自覚に「全記事同じ画像」を返す経路がある —
if (no_thumb) return LP_DEFAULTの fallback は「親切」だが、アイキャッチを設定しない運用と組み合わさると全記事同一画像という事故になる。OGP ロジックは定期的に 42 本一斉 curl などで実態確認すべき - 手動アイキャッチは工数とトーンの両方が負債になる — 工数の累積に加えて、地域別の「視覚シグナル差」(日本式ストック写真は海外で amateur に見える)も無視できない。動的生成は両方を構造的に回避する
- PHP GD + WP DB + ディスクキャッシュは 1 ファイルで成立する軽い構成 — 想像より重くない。初回 1 度の GD レンダリング → 以降は静的ファイル配信で、リクエストごとの負荷はほぼゼロ。Vercel / PlanetScale のような edge runtime が無くても十分間に合う
- 日本語フォントの併用は最初に詰む — Inter / Roboto などの欧文専用フォントに日本語タイトルを渡すと豆腐になる。
has_japanese()判定 + NotoSansJP の併用で言語別フォント選択を最初から組み込む
自社ブログの OGP を自社で語る、というメタな構造の記事になりましたが、この記事自身も動的 OGP 生成エンジンで作られたカードを持ちます。X や LinkedIn に貼った時、黒背景 + このタイトルの合成カードが表示されるはずです。テックブログを運営しているなら、似た事故が水面下で起きていないか、og:image を一度確認してみる価値はあると思います。