React アプリのリンクをサクッと Twitter で魅せる

# Twitterで魅せる とは
![image.png](https://qiita-image-store.s3.amazonaws.com/0/159680/f2c495b6-0c3a-5cc4-94ef-7945740f2f8a.png)
こういうやつ！ 名前は Twitter Card 。
リンクが Twitter 上でどう表示されるかは、[Card Validator | Twitter Developers](https://cards-dev.twitter.com/validator)で確認できる。

これを Reactアプリのリンクで表示したい！ という話。
最近初めて React を触っていざ Twitter Card 対応しようと調べた際、**SPA** における **OGP** 対応の悩みを知りました。 

[対応方法はいくつかあります](#参考)が、今回は筆者が実際に試した2つの方法を簡単に書きたいと思います。
実際に採用した「Static Site Generator の方法」 と 「Cloud Functions で meta タグを差し替える方法」の2つです。

# 方法1: SSG
Server Side Rendering (SSR) と比較して、Static Site Generator  (SSG) と呼ぶようです。
事前に JS を実行し各ページの HTML を生成してそれを配信する手法のこと。

筆者は小規模な個人アプリについてこの方法で OGP 対応しました。
具体的には <font color="ORANGE"><strong>react-snap + react-helmet</strong></font>  で HTML を生成し、それを Firebase Hosting で配信しています。[^1]

## [react-snap](https://github.com/stereobooster/react-snap)
ローカルでクロールして HTML を生成してくれるライブラリ。
(**名前に react って入ってるけど、別に react 専用のライブラリではなく Vue でも使えます！**)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/159680/bcaeda0c-8197-876b-cb22-82e30642557a.png)

package.json の scripts に `"postbuild": "react-snap"` を足すだけでよしなにやってくれる！

```package.json
"scripts": {
  "start": "react-scripts-ts start",
  "build": "react-scripts-ts build",
  "postbuild": "react-snap",
  "test": "react-scripts-ts test --env=jsdom",
  "eject": "react-scripts-ts eject"
}
```

これを使って手元で事前にレンダリングした結果を HTML として持つことができます。
ただ、これだけではどのページも同一の meta タグ (ルートのindex.htmlに書いたもの) になってしまいます。
そこで、react-helmet を導入してページ別に meta タグを設定するようにします。 

## [react-helmet](https://github.com/nfl/react-helmet)
```hello.tsx
import { RouteComponentProps } from 'react-router-dom'
import { Helmet } from "react-helmet";

export const Hello: React.SFC<RouteComponentProps<{}>> = props => (
  <div> 
    <Helmet
      title={'Hello World'}
      meta={[
        { name: 'twitter:card', content: 'summary' },
        { property: 'og:image', content: 'path/to/og_image' },
        { property: 'og:title', content: 'Helloページ' },
        { property: 'og:description', 'サンプルページです' },
        { property: 'og:url', content: `hoge_domain${props.path}` }
      ]}
    />
    <div> Hello! </div>
  </div>
)
```

こんな感じで Helmet コンポーネントを挿しこみ、meta 情報を指定できる。

この helemt を導入した上で `yarn build` すると、生成される HTML の meta タグは確かに動的に生成できている。
build/コンポーネント名/index.html を見るとこんな感じになってて嬉しい！

```build/Hello/index.html
<meta content="Helloページ" data-react-helmet="true" property="og:title">
```

## Firebase にデプロイ
いたってノーマルなままの設定でデプロイするだけ。

```firebase.json
{
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}
```

こんな感じでサクッといけちゃいました！

# 方法2: Cloud Functions による meta タグ差し替え
こちらについてはたくさんの記事があるので簡単に。
最初はこの方法で作ったのですが、小規模な個人開発かつシンプルなことしかしないアプリだったので、方法1の方が適していると考え切り替えました。

[Cloud Functions による動的コンテンツの配信](https://firebase.google.com/docs/hosting/functions?hl=ja) にも書いてあるように、meta タグ差し替えをすることで OGP 対応できます。
> SEO を向上させる単一ページアプリを事前レンダリングする。これにより、さまざまなソーシャル ネットワーク間で共有できる動的な meta タグを作成できます。

[Cloud Functions に TypeScript を使う](https://firebase.google.com/docs/functions/typescript) を参考にセットアップし、こんな感じで meta タグを差し替えます。

```functions/index.tsx
import * as functions from 'firebase-functions'
import * as fs from 'fs'

export const PreRender = functions.region('asia-northeast1').https.onRequest((req, res) => {
  res.set('Cache-Control', 'public, max-age=300, s-maxage=600')
  fs.readFile('./index.html', 'utf8', (e, html) => {
    const responseHtml = html.replace('トップページ', 'Helloページ') // meta　タグの中身を置換する処理を書く
    res.status(200).send(responseHtml)
  })
})
```

そして、Firebase Hosting の[リライト](https://firebase.google.com/docs/hosting/url-redirects-rewrites?hl=ja#section-rewrites)を使って、リクエストを Cloud Functions に渡します。

```firebase.json
{
  "hosting": {
    "rewrites": [
      {
        "source": "hello/*",
        "function": "PreRender"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
```

# おわりに
だいぶざっくりと書きましたが、参考になれば幸いです。

# 参考
## 公式ドキュメント
- [Optimize Tweets with Cards](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started.html)
- [Cloud Functions による動的コンテンツの配信](https://firebase.google.com/docs/hosting/functions?hl=ja)
- [Cloud Functions のロケーション](https://firebase.google.com/docs/functions/locations?hl=ja)
- [Cloud Functions に TypeScript を使う](https://firebase.google.com/docs/functions/typescript)

## ブログなど
- [【記事版】State of SEO for SPA 2018](https://qiita.com/seya/items/2688dc539af217b7b111) : とてもわかりやすいまとめ記事！ 調べながら色々苦戦する前にこの記事に出会いたかった...
- [JAMstack は眠らない](https://www.slideshare.net/kunichan/jamstack-106680339)
- [SSR無しでReactアプリをOGPとかに対応させる(自力prerendering編)](https://qiita.com/kiida/items/07e2c6ccb9e3ea230b16#_reference-9f08cc8eca79b8664110) : 本記事と同じくローカルで Pre Rendering する手法。
- [Firebase で SPA するときの SEO/OGP 対応もうこれでいいんじゃないですか 2018 暫定版](https://speakerdeck.com/tomomichi/ogpdui-ying-moukoreteiinsiyanaitesuka2018zan-ding-ban)
- [Firebase でサーバレスな SPA アプリを作った話②](https://qiita.com/g0e/items/75429eb805f7879a6fba)


[^1]: [React + TypeScript + Firebase Hosting の構成](https://qiita.com/sensuikan1973/items/4f39d1927f83f0664acc)をとっています