最初に
ジャンプ+、読んでいますか?
もし読んでいないなら、ゴーストフィクサーズの1話をぜひ読んでから次に進んでください。
https://shonenjumpplus.com/episode/9324103658562874562
前段
ジャンプ+では2025年の年末に【#あなたのジャンプラ2025】というキャンペーンをしていました。
最近よくある、1年間の総括をしてくれるやつですね。
診断結果をXに共有すると、URLの文字列の代わりにOGPカードを表示してくれます。
twitterの画面上はこんな感じで表示されています。

Qiitaでもやってくれるみたいですね。
……本当はこれらの漫画について語りたいところですが、今回は、OGPカードの画像部分についてです。この画像はユーザーごとのベスト5が表示されるようになっているのですが……それって大変そうな気がします。ジャンプ+ユーザーでこのサービスを使った人の分だけ画像を作るのか?
自分が開発しているサービスでもtwitterにユーザーごとの画像を表示したい、と考えていたので実装について調べてみます。
定義
この記事では、OGP関連の用語を以下のように定義しています。
- 各SNSがOGPから生成するカードのことをOGPカードと呼びます。
- OGPカードで表示される画像のことをOGP画像と呼びます。
OGPについて
OGPはOpen Graph Protocolの略で、リンクのカードを表示するための仕組みです。
ウェブサイトのヘッダに指定しておくと各SNSのbotが読み取ってOGPカードを表示してくれます。
あなたのジャンプラ2025はこんな感じで指定してありました。
今回はOGP画像の指定方法について見ていきます。
<head>
……
<title>あなたのジャンプ+2025|少年ジャンプ+</title>
<meta property="og:title" content="あなたのジャンプ+2025|少年ジャンプ+">
<meta property="og:url" content="https://shonenjumpplus.com:8080/anatanojumpplus/40f8ffac31994bb3bd050f8bc6a43233?share_image_type=most_read_works">
<meta property="og:type" content="website">
<meta property="og:image" content="https://cdn-anatano-img.shonenjumpplus.com/2025?data=(省略)">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:site_name" content="少年ジャンプ+">
<meta property="og:description" content="今年のジャンプ+読書歴を振り返ろう!">
<meta name="description" content="今年のジャンプ+読書歴を振り返ろう!">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@shonenjump_plus">
<meta name="twitter:title" content="あなたのジャンプ+2025|少年ジャンプ+">
<meta name="twitter:description" content="今年のジャンプ+読書歴を振り返ろう!">
<meta name="twitter:image" content="https://cdn-anatano-img.shonenjumpplus.com/2025?data=(省略)">
……
</head>
調査
OGP画像はog_:imageとtwitter:imageに指定する必要があります。
上記では省略しましたが、実際はこうなっています。
<meta property="og:image"
content="https://cdn-anatano-img.shonenjumpplus.com/2025?data=eyJhbGciOiJIUzI1NiJ9
.eyJ0eXBlIjoibW9zdF9yZWFkX3dvcmtzIiwid29ya3MiOlt7InJhbmsiOiIxIiwidGh1bWJuYWlsVXJsIjo
iaHR0cHM6Ly9jZG4tYWstaW1nLnNob25lbmp1bXBwbHVzLmNvbS9wdWJsaWMvc2VyaWVzLXN1Yi10aHVtYm5
haWwtc3F1YXJlLXdpdGhvdXQtbG9nby85MzI0MTAzNjI5MTUyMzU5Njc1LTMzNjEyMTllNjM2MGIyMThjYjd
hN2NiZWU4M2FjYTBkPzE3NTg4NjY2MTkifSx7InJhbmsiOiIyIiwidGh1bWJuYWlsVXJsIjoiaHR0cHM6Ly9
jZG4tYWstaW1nLnNob25lbmp1bXBwbHVzLmNvbS9wdWJsaWMvc2VyaWVzLXN1Yi10aHVtYm5haWwtc3F1YXJ
lLXdpdGhvdXQtbG9nby8zMjY5NzU0NDk2ODQwMDMxNTA1LTE3MzNkMDQ1Y2EzOWM1MGE5Yzg0NGUyZDBkZmE
3ZmNkPzE3NDI4MDQ4MDUifSx7InJhbmsiOiIzIiwidGh1bWJuYWlsVXJsIjoiaHR0cHM6Ly9jZG4tYWstaW1
nLnNob25lbmp1bXBwbHVzLmNvbS9wdWJsaWMvc2VyaWVzLXN1Yi10aHVtYm5haWwtc3F1YXJlLXdpdGhvdXQ
tbG9nby8xNzEwNjU2NzI1NDYyNzA0MDc4NC00ZGNjNGE1OTYyZDAzOWNkNDdhMTQ3YzQzYWNkNDYzOT8xNzU
4ODQ3Njk4In0seyJyYW5rIjoiNCIsInRodW1ibmFpbFVybCI6Imh0dHBzOi8vY2RuLWFrLWltZy5zaG9uZW5
qdW1wcGx1cy5jb20vcHVibGljL3Nlcmllcy1zdWItdGh1bWJuYWlsLXNxdWFyZS13aXRob3V0LWxvZ28vMTc
xMDYzNzE4OTI4MDQyMzIwNjctNzMyZjJlN2I3MTdlMzE4MTU4ZGZhOWZkYmQ0ODE3Mjc_MTc0MjQ4Mjg2NiJ
9LHsicmFuayI6IjUiLCJ0aHVtYm5haWxVcmwiOiJodHRwczovL2Nkbi1hay1pbWcuc2hvbmVuanVtcHBsdXM
uY29tL3B1YmxpYy9zZXJpZXMtc3ViLXRodW1ibmFpbC1zcXVhcmUtd2l0aG91dC1sb2dvLzEwODM0MTA4MTU
2NjYxMTM1OTI3LTc5NjMwNTFlOTA2ODQwMWMzNGZlYjc2ODRlNTExZmY3PzE3NjgyODU3NzcifV19
.BSmKS8vo6mmQwt-HvFvEfXk7Txod_idZ0jsm-9PlDec">
よく見てみると、dataに渡されている値はJWTです。復号してみると、こんな感じになっています。
{
"alg": "HS256"
}
{
"type": "most_read_works",
"works": [
{
"rank": "1",
"thumbnailUrl": "https://cdn-ak-img.shonenjumpplus.com/public/series-sub-thumbnail-square-without-logo/..."
},
{
"rank": "2",
"thumbnailUrl": "https://cdn-ak-img.shonenjumpplus.com/public/series-sub-thumbnail-square-without-logo/..."
},
{
"rank": "3",
"thumbnailUrl": "https://cdn-ak-img.shonenjumpplus.com/public/series-sub-thumbnail-square-without-logo/..."
},
{
"rank": "4",
"thumbnailUrl": "https://cdn-ak-img.shonenjumpplus.com/public/series-sub-thumbnail-square-without-logo/..."
},
{
"rank": "5",
"thumbnailUrl": "https://cdn-ak-img.shonenjumpplus.com/public/series-sub-thumbnail-square-without-logo/..."
}
]
}
つまり、https://cdn-anatano-img.shonenjumpplus.com/2025でOGP画像を生成できるように
- なんの情報なのか
- 1位から5位の画像リンク
を渡しているみたいです。
ここでURLに立ち戻ってみます。
-
https://shonenjumpplus.com/→ドメイン -
anatanojumpplus/40f8ffac31994bb3bd050f8bc6a43233→診断idを持つエンドポイント -
?share_image_type=most_read_works→たくさん読んだ漫画を示す
という形式なので、おそらく
- 診断idとshare_image_typeから、ランキングを取得
- ランキング情報をJWT化
- JWTをOGP画像のパラメータとして付加する
- OPGのbotはパラメータの値から生成された画像を受取り、SNSに表示する
というのが大雑把な流れになっているのかな、と推測しました。
実験
動的OGPの仕組みは理解したので、シンプルな動的OGPカードを作成してみます。
動的OGPの最小構成としては以下となるでしょう。
- URLに動的なパラメータを持たせる
- そのパラメータを使ってog:imageのurlを生成する
ここではtoday's NOとして、getで渡した数字の画像をOGPとして表示してみます。
フォルダ構成とかは端折ってますが、cloudFlareWorkersにこんな感じのソースをデプロイしてみます。
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
// /today?no=1 shows today's number page (only 1-9 allowed).
if (path === "/today") {
const no = url.searchParams.get("no");
if (!no || !/^[1-9]$/.test(no)) {
return new Response("Bad Request", { status: 400 });
}
return renderTodayPage(url, no);
}
return new Response("Not Found", { status: 404 });
},
};
function renderTodayPage(url: URL, no: string): Response {
const numberImagePath = `/number/${no}.png`;
const ogImageUrl = `${url.origin}${numberImagePath}`;
const numberImageTag = `<img src="${numberImagePath}" alt="Today's No" >`;
const html = `<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Today's No: ${no}</title>
<meta property="og:title" content="Today's No: ${no}">
<meta property="og:type" content="summary_large_image">
<meta property="og:image" content="${ogImageUrl}">
<meta property="og:description" content="Today's No is ${no}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Today's No: ${no}">
<meta name="twitter:image" content="${ogImageUrl}">
</head>
<body>
<div style="text-align: center; margin-top: 50px;">
<h1>Today's No: ${no}</h1>
${numberImageTag}
</div>
</body>
</html>`;
return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
GETしたnoをogのパスとして渡すのとページの画像表示に使うだけのシンプルな構成です。
OGP画像を動的に切り替えることができました!
ここから、
- ランダムな数字が選ばれるページを作ってこのページに飛ばした上で共有できるようにする
- 複数桁表示を許す
みたいなカスタムも面白いかもしれませんね。
あなたのジャンプラは単に画像を参照するのではなく、遷移先でパラメータを元に作られた画像が返ってくる感じなのでサーバ上での画像合成が必要になってきますね。今回はそこまでやらないですが。
余談
ウェブブラウザ上だと、twitterに対して自動で画像を添付するのにコストがかかります。
intentを使った共有だと画像添付はできないし、APIは馬鹿みたいな値段になる。
自ずとOGP画像を切り替える形が、現実的な実装なんだな、と思った。
(スマホアプリ→twitterアプリはできるんだけどなあ..)
最後に
おかえり水平線もみて!
https://shonenjumpplus.com/episode/17106567264307963664