「記事をXでシェアしたら画像が出ない」「サムネイルを差し替えたのに、前の画像がずっと残る」——制作会社のディレクターから、公開直後にこの連絡をもらうことがよくあります。OGP(SNSでシェアしたときに出るカードのこと)は、原因さえ切り分けられれば大半はその日のうちに直せます。この記事では、まず出ない・古いの定番原因を一つずつ潰し、そのうえで記事ごとにタイトルを差し込む動的なOGP画像をCloudflareのエッジで作る方法まで踏み込みます。

OGPが表示されない、よくある原因

最初に前提を一つ。SNSの巡回ロボット(クローラー)は、ページのHTMLにある og:image などのメタタグを読んで、カードを組み立てます。だから原因のほとんどは「メタタグの書き方」か「クローラー側の都合」に集約されます。

OGP画像が出ない・古い 定番原因
  • 相対パスで書いている。これが一番多いです。og:image は絶対URL(https:// から始まるフルのアドレス)で指定します。/ogp.png のような相対パスだと、クローラーはどのドメインの画像か判断できません。
  • メタタグが重複している。テーマとプラグイン、あるいはCMSと手書きで og:image が二重に出力されると、どちらが採用されるか読めません。head内に1つだけ、が原則です。
  • 画像そのものを指定していないog:image が無ければ、SNSは本文から適当な画像を拾うか、何も出しません。最低限のデフォルト画像は必ず用意します。
  • 画像が小さすぎる/取得できない。推奨サイズは媒体や時期で変わりますが、横1200×縦630がひとつの目安とされています。極端に小さいと無視されたり、正方形の小さなサムネイルになったりします。画像URLが404を返す、リダイレクトしている、robots.txtやファイアウォールでbotを弾いている、といった取得できないケースも実際に起きます。

自分のポートフォリオ(Astro+Cloudflare構成)では、この相対パス問題を根っこから断つために、OGP画像を出力する箇所で必ず絶対URLへ変換しています。ページ個別の指定が無ければ全ページ共通の /ogp.png に、記事ページなら記事のサムネイルに、いずれも new URL(パス, サイトのオリジン) を通してフルのURLに直してから og:image に渡す。こうしておくと、相対パスが紛れ込む余地がなくなります。この考え方は構造化データとOGPでSEO/AIOを強くする話でも触れています。

直したのに古い画像が残る、を解決する

メタタグを正しく直したのに、シェアすると前の画像が出続ける。これはあなたのサイトのせいではなく、SNS側のキャッシュ(一度取得した情報を保存して使い回す仕組み)が原因です。

各社ともカードの情報を一定期間ためておき、その間は再取得しません。保持する長さは媒体ごとに違い、数時間から数日と幅があるとされています。放っておいても期限が切れれば新しい画像に入れ替わりますが、公開直後に正しく出したいときは待っていられません。

打ち手は決まっています。

  • 各SNSが用意しているデバッガ(プレビュー確認ツール)で、対象URLを再取得(再スクレイプ)させる。これでその媒体のキャッシュが更新されます。
  • どうしても差し替えたいときは、画像URLの末尾に ?v=2 のようなクエリを足して、別URL扱いにする奥の手もあります。

公開フロー自体を整えておくのも効きます。私はサムネイルを確定させてから公開し、公開後にデバッガでひと通り再取得をかける、という順番を決めています。「後で直せばいい」で進めると、拡散のピークに古い画像が出回ってしまう。順番の問題なんです。

エッジで、記事ごとの動的OGPを生成する

ここからが一歩進んだ話です。ブログや商品ページのように件数が増えるサイトで、カードに記事タイトルを載せたい。1枚ずつ手で画像を作るのは現実的ではありません。そこで、リクエストが来たときにその場でタイトルを差し込んだ画像を生成する「動的OGP」が候補になります。

静的OGP と エッジで動的生成

Cloudflare Workers(エッジ=利用者に近い場所で動く実行環境)と相性が良いのは、ブラウザを立ち上げずに画像を作る方式です。一般的には、JSXとCSSのような記述からSVGを組み立て、それをPNGへ変換する、という流れが使われます。ブラウザ(Puppeteer等)を使わないので軽く、エッジの実行環境でも動かせるのが利点です。

イメージとしては、こんなエンドポイントを1本用意します。

// /og?title=... を叩くと、その場でOGP画像(PNG)を返すWorker
export default {
  async fetch(req: Request) {
    const url = new URL(req.url);
    const title = url.searchParams.get("title") ?? "Tsudzuri";
    // 1) title を差し込んだテンプレートから SVG を組み立てる
    // 2) SVG を PNG に変換して返す(ブラウザレス)
    const png = await renderOgImage(title); // 実装は生成ライブラリに委譲
    return new Response(png, {
      headers: {
        "Content-Type": "image/png",
        // 生成結果はエッジ/CDNでキャッシュして使い回す
        "Cache-Control": "public, max-age=31536000, immutable",
      },
    });
  },
};

あとは各ページの og:imagehttps://example.com/og?title=記事タイトル に向ければ、記事ごとに違うカードが出ます。実装で気をつけたい勘所を挙げておきます。

  • 出力形式。この方式の生成ライブラリはPNGやJPEGを前提にしているものが多く、WebPで作ろうとすると失敗する場合があります。素直にPNGで返すのが無難です。
  • フォント。日本語を載せるなら、使う文字をカバーするフォントをWorkerに同梱する必要があります。ここを忘れると豆腐(□)になります。
  • キャッシュ。毎リクエストで生成すると無駄なので、生成結果はエッジ/CDNでキャッシュして使い回します。タイトルが同じなら同じ画像になるので、キャッシュがよく効きます。
  • 外部画像を取り込むとき。背景などに別ドメインの画像を使う場合、取得元がbotっぽいアクセスを弾くことがあります。適切なヘッダを付けて取りに行く配慮が要ります。

Cloudflareに載せる全体像はEmDashをCloudflare(Workers+D1+R2)にデプロイする話で書いたとおりで、サーバーを別に立てずにこうした動的処理をエッジへ寄せられるのが、この構成の強みです。

メタは「壊さない運用」まで含めて設計する

動的OGPまで組んでも、CMSの更新で og:image の出力が崩れたら元も子もありません。テンプレート側で絶対URL変換を一箇所に集約し、記事の入力値に依存しすぎない作りにしておく。この「更新しても壊れない」観点は構造化データを壊さないCMS運用と地続きです。OGPも構造化データも、入れて終わりではなく、運用で崩れない設計まで込みで初めて価値が出ます。

OGPが出ない・古い問題は、切り分けの順番さえ決まっていれば怖くありません。動的生成は「あると差がつく」実装で、件数の多いサイトほど効いてきます。「うちのサイトでも記事ごとのカードを出したい」「シェアしても画像が出ないのを直したい」——そんなときは、実装の相談からお受けします。