0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【#あなたのジャンプラ2025】で知った、動的OGPについて

Posted at

最初に

ジャンプ+、読んでいますか?
もし読んでいないなら、ゴーストフィクサーズの1話をぜひ読んでから次に進んでください。
https://shonenjumpplus.com/episode/9324103658562874562


前段

ジャンプ+では2025年の年末に【#あなたのジャンプラ2025】というキャンペーンをしていました。
最近よくある、1年間の総括をしてくれるやつですね。

診断結果をXに共有すると、URLの文字列の代わりにOGPカードを表示してくれます。
twitterの画面上はこんな感じで表示されています。
image.png

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_:imagetwitter: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です。復号してみると、こんな感じになっています。

header.json
{
  "alg": "HS256"
}
payload.json
{
  "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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?