※これは2021年3月の記事です。botの仕様により動作しなくなる恐れがあります

対処法だけ見たい人は下にスクロール

AngularでOGP対応の限界と対処

TwitterやFacebook、DiscordなどにURLを貼ると画像つきリンクになるがAngularで実装するとなると限界がある。

「Angular OGP」で調べてみると@angular/platform-browser/Metaを使った記事が見つかる。静的なページだと問題ないがHttpClientなどを使用し動的にOGP情報を変更するとなると動作しない

OGP情報はいつ読み取られるのか

Twitterの公式Forumsに記載がある

Card error, unable to render, or no image: READ THIS FIRST - Cards - Twitter Developers

is your page adding the tags after it is loaded, for instance using JavaScript (e.g. Angular, Meteor, Google Tags Manager)? The crawler and validator cannot execute JavaScript and the tags must be static.

つまりTwitterではJavaScriptにて動的にmeta情報を変えたとしてもJavaScript起動前の情報を読み込むため反映されないと言うこと。ブラウザ上では表示されていてもbotには読まれていない。これはFacebookやDiscordにも言える。

対処法

結論から言うとAngularでは対処が不可能である。では、どうするかと言うとbot用のページを用意する。

WebサーバーでUserAgentがfacebook、twitter、discordのbotであった場合はOGPを適用した静的ページを返すようにする。箱庭はnginxをproxyサーバとして使用しているためこのような設定にしている。

例として箱庭ではhttps://hako.v-kurore.com/u/[ユーチューブチャンネルID]はユーザー単一ページを示す。これがbotによるリクエストの場合はOGP対応した静的なHTMLを返すhttps://api.v-kurore.com/api/v1/google/seo/[ユーチューブチャンネルID]に書き換える。

server {
    server_name hako.v-kurore.com;
    listen 80;
    access_log /var/log/nginx/access.log vhost;
    location / {
        if ($http_user_agent ~* "(facebookexternalhit/[0-9]|Twitterbot|Discordbot)") {
            rewrite ^/u/(.*)$ https://api.v-kurore.com/api/v1/google/seo/$1 permanent;
        }
        proxy_pass http://hako.v-kurore.com;
    }
}
1
2
3
4
5
6
7
8
9
10
11

下記はhttps://api.v-kurore.com/api/v1/google/seo/のHTMLを返す処理。DBから必要な情報を取得しHTML文を返す。最低限必要な内容はOGPに必要な情報のみとなるが、一応bodyにも簡単な情報を記載している。

  async seo(channelId: string): Promise<string> {
    const user = await this.userService.findOne(channelId);
    if (!user) {
      return '';
    }
    const description = user.youtubeDescription
      .replace(/\r?\n|/g, '')
      .replace(/\x20+|\u3000+/g, ' ')
      .slice(0, 100);
    const title = `${user.youtubeChannelName} - 箱庭`;
    const youtubeChannelId = user.youtubeChannelId;
    const youtubeThumbnailsUrl = user.youtubeThumbnailsUrl;
    const youtubeChannelName = user.youtubeChannelName;
    return `
    <!DOCTYPE html>
    <html lang="ja">
      <head>
        <title>${title}</title>
        <meta name="keywords" content="${youtubeChannelId},${youtubeChannelName}" />
        <meta charset="utf-8" />
        <meta name="description" content="${description}" />
        <meta property="og:title" content="${title}" />
        <meta property="og:type" content="website" />
        <meta property="og:description" content="${description}" />
        <meta property="og:url" content="https://hako.v-kurore.com/u/${youtubeChannelId}" />
        <meta property="og:image" content="${youtubeThumbnailsUrl}" />
        <meta property="og:locale" content="ja_JP" />
        <meta name="twitter:card" content="summary" />
        <meta name="twitter:image" content="${youtubeThumbnailsUrl}" />
        <meta name="twitter:description" content="${description}" />
        <meta name="twitter:title" content="${title}" />
        <meta name="twitter:url" content="https://hako.v-kurore.com/u/${youtubeChannelId}" />
      </head>
      <body>
      <h1>${youtubeChannelName}</h2>
      <p>${description}</p>
      </body>
    </html>
  `;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

確認

TwitterとFacebookは確認ページが用意されている。

Twitter→Twitter Cards Validator

Facebook→シェアデバッガー - Facebook for Developers