SSR無しでReactアプリをOGPとかに対応させる (自力prerendering編)

個人的にはServer side renderingはアーキテクチャ的に好きじゃないけどOpen Graph Protocolとかツイッターカードとかには対応したいなってことで、試みその1です。

# 追記

* 2017/6/3 - [Netlify編](http://qiita.com/kiida/items/bfb7844312c0539d4928)を書きました。
* 2017/6/9 - CSRのもう1つの問題である初期ロードが遅い問題に対応するために[Code-Splittingを導入する記事](http://qiita.com/kiida/items/c757dc6674f7e213fa64)を書きました。
* 2017/7/23 - [Lambda@Edge編](http://qiita.com/kiida/items/17e804765f424fd990db)を書きました。

# Prerenderとは

Prerenderはその名の通り事前にレンダリングして静的ファイルにすることです。静的ファイルになればWebサーバとかS3とかに置いて配信できるのでCDNの恩恵を受けたりOGPの対応ができたりします。Prerenderは[Prerender.io](https://prerender.io/)みたいなサービスを使うのがベターな気がしますが、今回はとりあえず自前でやってみます。

ちなみに今回のソースは[ここ](https://github.com/daikikohara/no-ssr-demo/tree/master/prerender-demo)にあります。

# 手順

大体の流れは[create-react-appのREADME](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#pre-rendering-into-static-html-files)に書かれています。

## Reactアプリを作る

ということでデモには[create-react-app](https://github.com/facebookincubator/create-react-app)で十分だと思うのでこれを使っていきます。

```bash:
yarn create react-app prerender-demo
```

上記は初回実行時の場合はsudo付けないとエラーになります。

## 必要なパッケージを追加

とりあえずルーティング用に[react-router](https://github.com/ReactTraining/react-router)を入れます。ルートごとのヘッダ部の書き換えには[react-helmet](https://github.com/nfl/react-helmet)を使います。helmet使っても実行中のユーザのヘッダを書き換えるだけで、実際にそのパスをシェアしても適切なリンクにはなりません。これを解決するために[react-snapshot](https://www.npmjs.com/package/react-snapshot)を使います。また、[react-helmetの最新バージョンはreact-snapshotと一緒に使えないので](https://github.com/geelen/react-snapshot/issues/17)4系を使います。

```bash:
cd prerender-demo
yarn add react-router-dom react-helmet@4
yarn add -D react-snapshot
```

## 書き換え

### react-routerとreact-helmet

今回はテスト用に2つのルートだけ作ります。まずは`App.js`を書き換えます。

```javascript:App.js
import React, { Component } from 'react';
import Helmet from 'react-helmet';
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom'
import logo from './logo.svg';
import './App.css';
import {
  Home,
  About
} from './containers';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Helmet title="Default title" />
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          <Router>
            <div>
              <ul>
                <li><Link to="/">Home</Link></li>
                <li><Link to="/about">About</Link></li>
              </ul>
              <hr/>
              <Route exact path="/" component={Home}/>
              <Route path="/about" component={About}/>
            </div>
          </Router>
        </p>
      </div>
    );
  }
}

export default App;
```

次にそれぞれのルート用のコンテナを作りてきとーにヘッダ情報を書いていきます。

```javascript:Home.js
import React from 'react';
import Helmet from 'react-helmet';

const Home = () => (
  <div>
    <Helmet
      title="Home"
      meta={[
        { name: 'twitter:card', content: 'summary' },
        { name: 'twitter:title', content: 'Home' },
        { name: 'twitter:description', content: 'description of Home' },
        { name: 'twitter:image', content: 'http://path/to/image' },
        { property: 'og:title', content: 'Home' },
        { property: 'og:type', content: 'website' },
        { property: 'og:url', content: 'http://path/to/this/url' },
        { property: 'og:image', content: 'http://path/to/image' },
        { property: 'og:description', content: 'description of Home' },
      ]}
    />
    <h2>Home</h2>
  </div>
)

export default Home;
```

About.jsはHomeをAboutに置換して画像を変えただけなので省略します。

### react-snapshot

次にreact-snapshot向けの変更として、`package.json`の`build`セクションを以下のように変更します。

```json:package.json
"build": "react-scripts build && react-snapshot"
```

そしてindex.jsで使うrenderをreact-snapshotのrenderメソッドに変更します。

```javascript:index.js
import { render } from 'react-snapshot';
(略)
render(<App />, document.getElementById('root'));
```

これで`yarn build`すると`build`ディレクトリ以下に静的ファイルが出来上がります。
ただし、about.htmlができてしまうので、ルートを/aboutから/about.htmlに変更するかabout.htmlをaboutにリネームしてください。

# 配置して確認

あとはこれを配置して確認します。今回はテスト用に確保しておいたS3に置いてCloudFront経由で配信しました。

## 表示

以下のように問題なくブラウザから確認できました。
ちなみに諸事情によりドメインは伏せています。

* Home
![home.png](https://qiita-image-store.s3.amazonaws.com/0/36003/e4444c68-e379-1730-0c99-857c4116993d.png)

* About
![about1.png](https://qiita-image-store.s3.amazonaws.com/0/36003/4eee63f6-a377-201d-37dd-589210942f5f.png)

## Slackで確認

Slackは[oEmbedやOGPやツイッターカードの情報を見てくれる](https://api.slack.com/robots)ので、試しにSlackに貼ってみたところ、ルートごとにきちんとOGPの情報が取得できることも確認できました。`/`では自分のプロフィール画像、`/about`ではQiitaのロゴを設定しましたが、設定通りに表示されておりdescriptionも反映されています。

![slack.png](https://qiita-image-store.s3.amazonaws.com/0/36003/94cd4404-3907-ec98-9d62-7da056557c27.png)

# 最後に

という感じで結構簡単にOGP対応ができました。でもreact-snapshotのページにはexperimentalって書いてあるので、もう少しまともなアプリがちゃんと動くかも今度確認したいと思います。というかプロダクションとかでちゃんと使うなら最初に述べたPrerender.ioみたいなサービスを使った方が良い気もします。この辺は試したら追記したいと思います。

ただ恐らくprerender以外の方法でやるつもりでいるので、次回はまた別の方法を紹介したいと思います。