GAになったLambda@Edgeを使ってSPAをSSR無しでOGPとかに対応させてみる

ということで、[前回](http://qiita.com/kiida/items/07e2c6ccb9e3ea230b16)と[前々回](http://qiita.com/kiida/items/bfb7844312c0539d4928)の続きでServer Side Rendering無しでOGPとかに対応する試みその3です。

今回は先ほど[GAになったLambda@Edge](https://aws.amazon.com/blogs/aws/lambdaedge-intelligent-processing-of-http-requests-at-the-edge/)を使ってみます。前回から時間が空いたのは[Lambda@Edge](http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html)が中々Previewを抜けず、またPreview中の情報公開は[規約違反](https://aws.amazon.com/service-terms/)になってしまうためです。

# Lambda@Edgeとは

Lambda@Edgeは簡単に言うとCloudFrontのいくつかのイベントをトリガーにLambda関数を実行する機能です。UAによって返すコンテンツを変えたりできるようになるので、ABテストとかに利用できます。今回はこれを使って、一般的なブラウザなら本来のアプリを表示し、それ以外の場合は静的なコンテンツを返すことでOGP対応してみます。

# 手順

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

## Reactアプリを作る

前回同様create-react-appで作ります。

```
yarn create react-app edge-demo
```

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

今回はreact-router-domとreact-helmetのみ使います。helmetはOGPとかに対応するだけなら必要ありませんが普通にアプリを表示させた場合にもヘッダを書き換えたいので入れてます。

```
yarn add react-router-dom react-helmet
```

## ルートを作ってビルドする

前回とほぼ同じ内容なので省略しますが、HomeとAboutの二つのルートを作ります。

そしてyarn buildするとbuildディレクトリ以下に静的ファイルが出来上がります。前回のprerenderと違ってabout.htmlは作られずindex.htmlしか作られないのでこれをS3に配置しただけだとOGPの情報は拾えませんが、一旦これをS3に配置します。

## Lambda関数を作る

AWS Lambdaのコンソールからviewer-requestをトリガーとした以下のような関数を作ります。

```
'use strict';

const whitelist = [
  'chrome',
  'crios', // chrome for ios
  'firefox',
  'fxios', // firefox for ios
  'googlebot', // since google bot can render javascript. TODO: update to react-router v4 on front
];

const isSupportedBrowser = (uas) => {
  if (uas && Array.isArray(uas) && uas.length > 0) {
    return uas.some(ua => whitelist.some(w => ua.value.toLowerCase().indexOf(w) !== -1));
  }
  return false;
};

exports.handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const headers = request.headers;
  const ua = 'user-agent';

  if (!isSupportedBrowser(headers[ua])) {
    if (!request.uri || request.uri === '/' || request.uri.indexOf('index.html') !== -1) {
      request.uri = '/index.static.html';
    } else {
      request.uri += '.static.html';
    }
  }
  callback(null, request);
};
```

かなりいい加減ですが、Chromeの場合とgooglebotの場合はそのままアプリを表示、それ以外のUAの場合はパス+.static.htmlというファイルを表示するようにしています。
ちなみに静的ファイルのパスに書き換えるだけじゃなく、レスポンスの内容を動的に作って返すこともできますが、自動的にマッピングした静的ファイルを作成するような仕組みを考えているのでこのようにしました。
というか、この辺はどうするのがベストか模索中なのでオススメの方法があれば教えて頂けると嬉しいです。(というか今後出てくると思ってます)

## .static.htmlを配置

上記でuriの書き換え先にしたindex.static.htmlとabout.static.htmlを配置します。

```html:index.static.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Home</title>
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="Home">
  <meta name="twitter:description" content="description of Home">
  <meta name="twitter:image" content="http://path/to/image">
  <meta property="og:title" content="Home">
  <meta property="og:type" content="website">
  <meta property="og:url" content="http://path/to/this/url">
  <meta property="og:image" content="http://path/to/image">
  <meta property="og:description" content="description of Home">
</head>
<body>
  Home
</body>
</html>
```

about.static.htmlは省略します。

## ブラウザから確認

まずはChromeで確認すると通常のアプリの画面が表示されます。画面自体は[前回のこの画面](http://qiita.com/kiida/items/07e2c6ccb9e3ea230b16#%E8%A1%A8%E7%A4%BA)と全く同じなので省略します。

## Slackで確認

前回同様Slackに貼るときちんとOGPの情報を解釈してルートごとに表示してくれています。
でも前回と同じ画像にしてしまったので、前回の[このスクショ](http://qiita.com/kiida/items/07e2c6ccb9e3ea230b16#slack%E3%81%A7%E7%A2%BA%E8%AA%8D)と見た目が全く同じになってしまいました。別のテキストや画像にすれば良かったですね。

## googlebot

ソースにある通りですが、googlebot向けには普通のアプリを返してます。これは前回まででgooglebotはネットワークリクエスト等のあるSPAでも問題なくindexできることが分かっているためと、[クローキング](https://support.google.com/webmasters/answer/66355?hl=ja)と見なされる可能性があるためです。(確認はしていません)

# 所感

このやり方の利点は、既存AWSユーザはそのままCloudFront使い続けられることと、既存のアプリには手を加えずに対応できる(マッピングさせる処理などは除く)ことだと思います。一部のURLしか対応しなくて良い場合は楽な選択肢なんじゃないでしょうか。

ただCloudFrontでしか使えないので、AWSユーザ限定になってしまいますね。~~また、実際のLambda関数は特定のリージョンで動くので結局CDNの利点が損なわれてる気もします。~~(追記：ブコメで指摘頂きましたが、ちゃんとEdge Locationで動くようです。[公式ドキュメント](http://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html#lambda-edge-how-it-works)によると作成はUS East (N. Virginia)だけど、レプリカがそれぞれのリージョンに作られるそうです。追記ここまで)あと真面目にマッピングさせたり変更時にInvalidateさせたりするとそれなりに手間なので、素直にSSRするなり、Prerendering使うなりした方が良いって結論になるかもしれません。

# 最後に

ということで簡単ですがLambda@Edgeを使ってSSR無しでOGPに対応させる方法の紹介でした。これで一旦SSR無しのOGP対応のネタは終わりの予定です。

個人的には今回の用途ならNetlifyみたいなサービスが一番だと思いますが、Lambda@Edge自体は今回以外の用途にも色々応用できるので非常に強力なサービスだと思います。

ということで、参考になれば幸いです。
実運用等して気付いた点があれば追記したいと思います。