クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【React】OGPはつらいよ ーSPAでの動的OGP・失敗編ー(Reactアプリスターターキット)

Reactスターターキットの続きです。前回はfavicon画像の作成、推奨サイズ、スマホ向け設定などを行いました。今回はSNSにシェアした時に表示されるOGPを設定します。helmetを使ってルーティングのページごとにOGPを設定しましたが、SPAでは動的なOGPはうまくいかず。。静的なOGPなら設定できました。それではいきましょう!

【目次】

※参考:前回記事
【React】favicon画像の作成、推奨サイズ、スマホ向け設定など(Reactスターターキット) - クモのようにコツコツと

※参考:【React】ReactでWebアプリを作るシリーズまとめ
qiita.com

前回のおさらい

favicon画像の設定。推奨サイズを調べて四種類の画像を作成。favicon作成ジェネレーターを利用してマルチアイコン作成、スマホ向け設定も。 https://cdn-ak.f.st-hatena.com/images/fotolife/i/idr_zz/20210414/20210414060406.jpg

※参考:【React】favicon画像の作成、推奨サイズ、スマホ向け設定など(Reactスターターキット) - クモのようにコツコツと

今回はSNSでシェアされた時に表示されるOGPを設定する。

OGPとは

OGPとは、「Open Graph Protcol」の略でFacebookやTwitterなどのSNSでシェアした際に、設定したWEBページのタイトルやイメージ画像、詳細などを正しく伝えるためのHTML要素です。

※参考:OGPを設定しよう!SNSでシェアされやすい設定方法とは?

OGPが設定されていないページをTwitterでシェアするとこのように青色のURLしか表示されない。

f:id:idr_zz:20210415065140j:plain

一方、OGPが設定されているページをシェアするとこのようにTwitterカードが表示される(この顔アイコンのドアップもどうかと思うがw)。

f:id:idr_zz:20210415065220j:plain

自分の作ったReactアプリでもカードが表示されるようにOGPを設定したい!

OGP画像の作成

まずカードに表示されるOGP画像の推奨サイズを調べる。

Twitter と Facebook の両方に対応させるには 600 x 315 以上(横幅が 600 )
高解像度端末に対応させたい場合は倍の 1200 x 630 以上

※参考:OGP image 画像のサイズ仕様( 2020 年時点) | gotohayato.com


このページのジェネレーターに大きめの画像をアップするとOGP画像のプレビュー確認と画像作成ができる!

※参考:OGP画像シミュレータ | og:image Simulator

やってみた。いつもSNSアカウントの背景画像などに使っている画像 f:id:idr_zz:20210415071000j:plain 元画像は正方形サイズだとクモがうまく入らなかったため、Figmaで縦横比や左右位置を調整した新画像を作った。

この画像(ogp.png)を「public」フォルダの直下に追加する。

HelmetでOGP設定

React-routerでルーティング設定しているページごとにOGPを設定したい。titleタグやdescriptionと同様にHelmetで設定してみる。

metaタグとして書く方法

※参考:OGP対応(react-helmet利用) - 奇をてらったテクノロジー

Helmetの属性として書く方法

※参考:React Helmetを使ってOGP対応した - akameco Blog

両方試したが、metaタグは見た目直感的だが、Helmetの属性として書く方法がシンプルに感じた。


「data.json」に繰り返し使うデータ(ドメイン、OGP画像、Twitterアカウント)を設定。

{
     "data": {
         "info": {
             "domain": "https://ryo-i.github.io",
             "ogpImg": "/ogp.png",
             "tw": "idr_zz"
         },
        // 後略

今回、OGPの画像は1枚で統一する予定(タイトル文字を合成させた画像を作ってるサイトとか見受けられるけどおそらくサーバ側での処理がありそうな気がするので今回はスコープ外とする)


Mainコンポーネント(Main.tsx )で先ほどのデータを取り込む

 const data = Data.data;
 // 中略
 const domain = data.info.domain;
 const ogpImg = data.info.ogpImg;
 const tw = data.info.tw;

MainページのOGPを設定

          <Route exact path={ homeUrl + "/" }>
            <Helmet
              title={ mainTitle }
              meta={[
                { name: 'description', content: mainText },
                { property: 'og:title', content: mainTitle },
                { property: 'og:type', content: 'website' },
                { property: 'og:url', content: domain + homeUrl + '/' },
                { property: 'og:image', content: domain + homeUrl + '/ogp.png' },
                { property: 'og:description', content: mainText },
                { name: 'twitter:card', content: 'summary_large_image' },
                { name: 'twitter:image', content: domain + homeUrl + '/ogp.png'},
                { name: 'twitter:site', content: tw },
              ]}
            />
            <h1>{ mainTitle }</h1>
            <p dangerouslySetInnerHTML={{ __html: mainText }}></p>
            <Inner />
          </Route>

ogpの基本設定

  • property属性をog:title, content属性をmainTitle
  • property属性をog:type, content属性をwebsite
  • property属性をog:url, content属性をdomain + homeUrl + '/'
  • property属性をog:image, content属性をdomain + homeUrl + '/ogp.png'
  • property属性をog:description, content属性をmainText

OGPのimageのURLは絶対パスにする必要がある。

※参考:OGPとは?設定するべき理由と設定方法について | デジマギルド

Twitterカード固有の設定

  • name属性をtwitter:card, content属性をsummary_large_image
  • name属性をtwitter:image, content属性をdomain + homeUrl + '/ogp.png
  • name属性をtwitter:site, content属性をtw

Twitterカードの属性の詳細

※参考:カードの利用開始 | Docs | Twitter Developer


続いてAboutページのOGPを設定

          <Route path={ homeUrl + "/about" }>
            <Helmet
              title={ aboutTitle }
              meta={[
                { name: 'description', content: aboutText },
                { property: 'og:title', content: aboutTitle },
                { property: 'og:type', content: 'article' },
                { property: 'og:url', content: domain + homeUrl + '/about' },
                { property: 'og:image', content: domain + homeUrl + '/ogp.png' },
                { property: 'og:description', content: aboutText },
                { name: 'twitter:card', content: 'summary_large_image' },
                { name: 'twitter:image', content: domain + homeUrl + '/ogp.png'},
                { name: 'twitter:site', content: tw },
              ]}
            />
            <h1>{ aboutTitle }</h1>
            <p dangerouslySetInnerHTML={{ __html: aboutText }}></p>
            <About />
          </Route>

先ほどと変えたところ

  • property属性をog:title, content属性をaboutTitle
  • property属性をog:type, content属性をarticle
  • property属性をog:url, content属性をdomain + homeUrl + '/about'
  • property属性をog:description, content属性をaboutText

titledescriptionをアバウトページの内容に。あと、og:typeは下層ページはarticleになる。


ここまでのコミット

※参考:GitHub - ryo-i/react-app-started at 79750ca98a83012ef9370a019d24e0d706e4d5bc

動的OGPの挙動確認(失敗)

ブラウザのDev-tooles、SourcesパネルにはOGP設定はないが f:id:idr_zz:20210416065623j:plain

ElementsパネルにはOGP設定がレンダリングされている f:id:idr_zz:20210416065305j:plain

これでうまくいくはず!と期待していた。


TwitterのOGP表示はこちらで確認できる。 f:id:idr_zz:20210416055105j:plain

※参考:Login on Twitter

--

まずMainページのURL(https://ryo-i.github.io/react-app-started/)を入れてみる。 f:id:idr_zz:20210416055703j:plain むむっ?

INFO: Page fetched successfully
INFO: 5 metatags were found
ERROR: No card found (Card error)

5つのメタタグが見つかりました、カードが見つかりません、と。 メタタグ自体は認識されているということか。画像を読む部分がエラー?


ちなみにAboutページ(https://ryo-i.github.io/react-app-started/about)はもっと認識されない。。 f:id:idr_zz:20210416060319j:plain

INFO: Page fetched successfully
WARN: No metatags found

メタタグ自体がありません、と。。

SPAでは動的OGPできない。。

調べるとSPA単体では動的OGPが設定できないっぽい。

www.slideshare.net

※参考:SPA時代のOGPとの戦い方

GooglebotのクローラーはJSのレンダリングを認識してくれるが、Twitter、Facebookのクローラーは認識してくれないと。。


実際、以前作った「ジャンプ率ジェネレーター」をGoogleで検索するとHelmetで設定したtitle(ブラウザ側でJSでレンダリングされている)がちゃんとヒットしている。 f:id:idr_zz:20210416061153j:plain

※参考:React App

HelmetはGoogleクローラーはちゃんと読んでくれるんだな。Twitter、Facebookも読んで欲しいものだ。。

index.htmlに静的OGPを直書き

動的OGPは簡単にでできなそうなので、ひとまずindex.htmlに静的OGPを直に書いてみる。

「data.json」に設定したOGP情報は削除

{
     "data": {
         /* "info": {
             "domain": "https://ryo-i.github.io",
             "ogpImg": "/ogp.png",
             "tw": "idr_zz"
         }, */

Mainコンポーネント(Main.tsx)のOGPの変数も削除

 // const domain = data.info.domain;
 // const ogpImg = data.info.ogpImg;
 // const tw = data.info.tw;

MainページのmetaタグからOGP設定を削除

            <Helmet
               title={ mainTitle }
               meta={[
                 { name: 'description', content: mainText }
               ]}
             />

AboutページもOGP設定を削除

            <Helmet
               title={ aboutTitle }
               meta={[
                 { name: 'description', content: aboutText }
               ]}
             />

ここまでのコミット

※参考:GitHub - ryo-i/react-app-started at a6a8b0d0e07985ea8870cee0d06ee4f6772a1190


「public/index.html」にOGPを直書き

    <title>Reactアプリスターターキット</title>
    <meta name="description" content="React + TypeScript + CSS in JS環境">
    <meta property="og:title" content="Reactアプリスターターキット">
     <meta property="og:description" content="React + TypeScript + CSS in JS環境">
     <meta property="og:url" content="https://ryo-i.github.io/react-app-started/">
     <meta property="og:image" content="https://ryo-i.github.io/react-app-started/ogp.png">
     <meta property="og:type" content="website">
     <meta name="twitter:site" content="@idr_zz">
     <meta name="twitter:image" content="https://ryo-i.github.io/react-app-started/ogp.png">
     <meta name="twitter:card" content="summary_large_image">

また、これを機にtitledescriptionも直書きにした。

静的OGPの挙動確認(成功!)

TwitterのOGP確認、リベンジ!

※参考:Login on Twitter

MainページのURL(https://ryo-i.github.io/react-app-started/)を入れると…

おお、今度は表示された! f:id:idr_zz:20210416063653j:plain

INFO: Page fetched successfully
INFO: 16 metatags were found
INFO: twitter:card = summary_large_image tag found
INFO: Card loaded successfully

16個のメタタグが見つかりました、Twitterカードの画像も見つかりました、と。


Aboutページ(https://ryo-i.github.io/react-app-started/about)を入れると…

あー、やはりこっちはダメかー。 f:id:idr_zz:20210416063902j:plain


MainページをTwitterに実際に投稿、OGPが表示された! f:id:idr_zz:20210416064151j:plain

下層ページは、やはりダメかー f:id:idr_zz:20210416064242j:plain

下層ページもMainページ扱いにしたいが難しそう

下層ページのURLでシェアされてもMainページのOGPが表示されちゃって構わないんだがなー。

思うに、og:urlのURLと実際のURLが一致していないのが問題な気がする。

<meta property="og:url" content="https://ryo-i.github.io/react-app-started/">

Aboutページはこの後に/aboutが続くので、それが存在しないページという認識になっているのではないかな(ちなみにindex.htmlとかパラメタ?aaa=bbbなどを繋げても問題はなかった)。


canonical(カノニカル)設定で全てのURLをMainページに統一化も試してみた

canonicalを設定すると、複数の重複ページが存在している場合に、検索エンジンに優先させるべきページを伝えることでそれ以外のページは重複ページであることを伝える事ができます。

※参考:canonical(カノニカル)とは?URLの正規化でSEOのマイナス評価を避けよう|ferret

しかしうまくいかなかった。そういう問題ではないんだな。


自作のSNSシェアボタンで設定できる内容も調べたが、コアな情報は結局OGPで設定する必要があるようだった。

※参考:オリジナルのシェアボタンを作ろう!各種SNSのボタン設置用URLまとめ | Web Design Trends

メモ:動的OGPを解決する方法

動的OGPの解決するにはOGP部分だけサーバ側で設定する必要がありそう。先ほどのスライドでも後半でAWS Lambda@Edgeを使って解決していた。

www.slideshare.net

OGP部分をExpressで設定する事例。

※参考:Reactで作ったページにTwitterCardsとOGPのメタデータを埋める - sambaiz-net

OGP部分だけKV型のDBと連携する方法もあるようだ。

※参考:SPAでも動的OGP対応したい! with Cloudflare Workers, KV


あとはNext.jsで設定する方法。 speakerdeck.com

※参考:選ばれたのは Next.jsでした - Next.jsによるServer Side OGP ⽣成 / Next.js was chosen - Server Side OGP generation with Next.js - Speaker Deck

複数ページになるならSPAでルーティングせずに最初からNext.jsでマルチページアプリケーション(MPA)として作るのがいいのかもなー、と今回も改めて考えさせられた。

最後に

ということで、OGP設定、紆余曲折の結果、今回はMainページの静的OGPのみ設定しました。

ブログやECサイトなど下層ページに固有な重要な情報がある場合はNext.jsが良さそうですが、ちょっとしたSPAアプリは下層ページはたいして重要ではないので、下層ページのOGPはいったん諦めることにします。

Next.jsはSSRではサーバ側をNode環境にする必要がありますが、今はSSG(静的サイト生成)という選択肢が増えてきたので、今後、MPA構成にする場合はNext.jsにもトライしていきたく思います。

※参考:Next.jsにおけるSSG(静的サイト生成)とISRについて(自分の)限界まで丁寧に説明する - Qiita

※参考:Next.js 4年目の知見:SSRはもう古い、VercelにAPIサーバを置くな - Qiita

それではまた!


※参考:【React】ReactでWebアプリを作るシリーズまとめ
qiita.com