🍘

SNS毎にOGP画像を変える裏技

に公開

結論

SNS毎にOGP画像を変える裏技は「SNS botのUserAgentごとに表示する画像を変える仕組みにする」です!
SNS毎にOGP画像を変える利点から実践まで順を追って説明します

OGP画像を出し分ける利点

まずOGP画像の重要性についてですが検索すると色々出てくるのですが、まとめると「適切なOGP画像が設定されているページはユーザーの目に留まりやすくなりクリックされる確率が高まる」という感じです。
これは明らかだと思います、Zennの記事もそうです。どちらが宣伝効果があるかは一目瞭然です。

OGPあり OGPなし

商品の販売を目的としたサイトだと商品画像をOGPにしたりと色々な工夫もあります

ここにさらにどの層のユーザが見ているかを意識するとさらに先の「適切なOGP画像」の適切さが高まると考えられます。
例えばXのユーザは文章を読むのになれているのでテキスト多めの画像にする、Facebookだったら年齢層は高めでスタイリッシュなデザインが合うなど、そのSNSのユーザの特徴やそのSNS自身のデザインに合わせた画像が適しているはずです。

また、SNS毎に推奨の画像サイズ、アスペクト比があり、SNSにあったサイズに変えられるという利点もあります。
(DeepResearchに調べてもらったところ、ほとんどのSNSが1200×630が最適でそれ以外はX: 1200×628, BlueSky: 1200×627, Mastodon: 1200×675らしいです)

SNSのOGP画像の管理方法

この記事を書く中で多くのSNSについて調べた結果、SNSのOGP画像の管理方法は大きく分けて以下の2通りあることがわかりました。

通称 概要 特徴
X(Twitter)型 投稿時に自サービス内にOGP画像を保存し、保存した画像を配信する 事前に画像を検閲できる
リンク元に負荷をかけない
mixi2型 OGP画像の生のURLでそのまま配信する 開発工数が最小限
画像変更が即時反映される

完全な調査はできていませんが、X(Twitter)型が多いように感じました。
ただし、X型と思われるものの中に「自サービスのProxyを介してOGP画像を取得して配信する」タイプが紛れているかもしれません...(即時反映+事前に画像を検閲できる機能を備えているのでなくはなさそうです)
実際にXでは以下のように7日間自社のサービス内にキャッシュされるようです

Content is cached by Twitter for 7 days after a link to a page with card markup has been published in a Tweet.

引用: Cards | Developer Platform

ただしこの形式の欠点としてリンク元のOGP画像変更の反映に時間がかかることがあります
(それを回避する裏技も色々ありそうです)

このZennでもhttps://embed.zenn.studio/api/optimize-og-image/xxxのようなURLからOGP画像が配信されており、上記リンクを開いてもにリンク元にアクセスがないように見えるのでX(Twitter)型を採用しているかもしれません
(そして私の見間違えかもしれないですがZennの記事にURLを貼るとXと全く同じUser-Agent: Twitterbot/1.0からのリクエストから来るので仕組みを一部共有している...?)

出し分けの実践

最後にSNSごとにOGP画像を出し分けるサンプルページを作ってみます!
最初に皆さんの多くが使っているであろうのX(旧Twitter), Facebook, Threads, Bluesky, mixi2, Misskey, Mastodon, Discord, SlackのUser-Agentを抽出してみます。

User-Agentの取得

アクセスされたらUser-Agentをログに残すようなサーバを起動して、実際に調べたいSNSに貼り付けてみます
まずはサーバを準備します、今回はデプロイを簡単にするためにHono+Cloudflare Workersで作りました
npm create hono@latest ua-checker作成したtemplateのsrc/index.tsを以下に書き換えnpm run deployでデプロイできます!
今回はCloudflare側のログを見たかったのでwrangler.jsoncにobservabilityを追加しておきます

src/index.ts
import { Hono } from 'hono';
import ogpImage from './image';

const app = new Hono();

// Middleware to log requests
app.use('*', (c, next) => {
  const requestUrl = new URL(c.req.url);
  const sns = requestUrl.searchParams.get('sns');
  const allHeader = c.req.header();

  let logHeaders = '';
  for (const [key, value] of Object.entries(allHeader)) {
    logHeaders += `${key}: ${value} `
  }

  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] ${c.req.method} ${requestUrl.pathname} SNS: ${sns} logHeaders: ${logHeaders}`);
  return next();
});

app.get('/image', (c) => {
  c.header('Content-Type', 'image/png');
  // OGP画像の表示に対応しているか確認するため念の為画像も表示している
  // RedditやタイッツーはOGP画像表示機能がないようだった
  return c.body(base64ToUint8Array(ogpImage), 200);
});

app.get('/', (c) => {
  const requestUrl = new URL(c.req.url);
  const sns = requestUrl.searchParams.get('sns');
  const ogpImageUrl = `${requestUrl.origin}/image?sns=${sns ?? ''}`;

  const html = generateHtml(ogpImageUrl);
  return c.html(html);
});

function generateHtml(ogpImageUrl: string): string {
  return `<!DOCTYPE html>
<html>
  <head>
    <meta property="og:image" content="${ogpImageUrl}" />
  </head>
</html>`;
}
wrangler.jsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "ua-checker",
  "main": "src/index.ts",
  "compatibility_date": "2025-05-24",
+  "observability": {
+    "enabled": true,
+    "head_sampling_rate": 1
+  }
}

これをnpm run deployでCloudflare Workersにデプロイして、以下のように媒体の名前をメモ代わりに変えながら、バシバシと投稿してCloudflareのログからRequest Headerを確かめてみます。

https://ua-checker.ponyo877.workers.dev?sns=platform-name

https://ua-checker.ponyo877.workers.dev?sns=platform-name

結果は以下です。
日本国内の主要なSNS/チャットのbotのUser-Agent一覧(2025年5月現在, @ponyo877調べ)

Product User-Agent Memo
X(Twitter) Twitterbot/1.0
Tumblr Tumblr/14.0.835.186
Facebook facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php) 他のheaderも同じ、なのでThreadsと同じシステムを使っているっぽいので見分け得られない...
Threads facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php) 同上
Bluesky Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Bluesky Cardyb/1.1; +mailto:support@bsky.app) Chrome/W.X.Y.Z Safari/537.36 初回以降は異なるのでcache方式ではないかもしれない
mixi2 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Refererがhttps://mixi.social/ なのでそれで見分けられそう
Misskey Mozilla/5.0 (compatible; SummalyBot/5.2.1-io.1)
Mastodon Mastodon/4.3.7 (http.rb/5.2.0; +https://social.ffmuc.net/)
Discord Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)
Slack Slackbot 1.0 (+https://api.slack.com/robots)
Line facebookexternalhit/1.1;line-poker/1.0

SNS毎にOGP画像が変わるページの作成

先に取得したUser-AgentやReferer(mixi2のみ)を元にSNSのbotやユーザ参照した時に生じるGET Requestのogp:imageをSNS毎に動的に変えてみます

src/index.ts
// Social media platform configurations
const PLATFORM_CONFIGS = [
  { userAgent: 'Twitterbot', endpoint: 'twitter' },
  { userAgent: 'Tumblr', endpoint: 'tumblr' },
  { userAgent: 'www.facebook.com', endpoint: 'meta' },
  { userAgent: 'Bluesky', endpoint: 'bluesky' },
  { userAgent: 'SummalyBot', endpoint: 'misskey' },
  { userAgent: 'Mastodon', endpoint: 'mastodon' },
  { userAgent: 'Discordbot', endpoint: 'discord' },
  { userAgent: 'Slackbot', endpoint: 'slack' },
  { userAgent: 'line-poker', endpoint: 'line' },
] as const

function detectPlatform(userAgent: string, referer: string): string {
  for (const config of PLATFORM_CONFIGS) {
    if (config.userAgent && userAgent.includes(config.userAgent)) {
      return config.endpoint
    }
  }
  return 'none'
}

// mixi2型のための対策
app.get('/image', (c) => {
  const referer = c.req.header('Referer') || ''
  if (referer.includes('mixi.social')) {
    const ogpImageURL = `${R2_ENDPOINT}/mixi2.png`
    return c.redirect(ogpImageURL, 302)
  }
  return c.redirect(`${R2_ENDPOINT}/default.png`, 302)
})

app.get('/', (c) => {
  const userAgent = c.req.header('User-Agent') || ''
  const referer = c.req.header('Referer') || ''
  console.log(`User-Agent: ${userAgent}, Referer: ${referer}`)

  const platform = detectPlatform(userAgent, referer)
  let ogpImageURL = `${R2_ENDPOINT}/${platform}.png`
  if (platform === 'none') {
    ogpImageURL = `${SELF_ENDPOINT}/image`
  }

  console.log(`ogpImageURL: ${ogpImageURL}`)

  const html = generateHtml(ogpImageURL)
  return c.html(html)
})

export default app

先ほどと同様にこれをCloudflare Workersにデプロイして完成です。
以下のURLになります、Xに貼ればXの画像、Threadsにはればthreadsの画像、主要なSNSは大体カバーしていると思います!

https://sns-ogp-switcher.folks-chat.com

以下例(全部同じURLの投稿です!)

Misskey Threads mixi2 Discord Slack
Tumblr Facebook Bluesky Mastodon Line

今回の検証で使ったコードは以下で管理しています
https://github.com/ponyo877/sns-ogp-switcher

まとめ

OGPの効果をより高めるために媒体毎にOGPを変える方法を紹介しました
いかがでしたでしょうか?

他にもOGPに関する記事をいくつか作っているので参照いただけたら嬉しいです。
https://zenn.dev/ponyo877/articles/8f5a4fb254ccd6
https://zenn.dev/ponyo877/articles/c776598994d17c

Discussion