Firebase + SPA + CloudFront + Lambda で SSR なしに OGP 対応


この記事は[Firebase + SPA で SSR なしに OGP 対応](https://qiita.com/zaru/items/829b4017a33df907f087)の改善版です。今回は CloudFront + Lambda@edge を使って Facebook / Twitter BOT の場合のみ別の URL へ振り分けてあげることで OGP に対応しようという試みです。

## Lambda@edge

Lambda@edge を使えば CloudFront へのリクエストの際に細かい制御をすることができます。今回は UserAgent を見て条件に一致したら OGP 用の URL へ変更するようにします。

```javascript
'use strict'

const bots = [
  'Twitterbot',
  'facebookexternalhit'
]

module.exports.redirect = (event, context, callback) => {
  const request = event.Records[0].cf.request
  const headers = request.headers
  const isBot = bots.some(v => {
    return headers['user-agent'][0].value.includes(v)
  })
  if (isBot) {
    request.uri = request.uri.replace(/^\/@note/g, '/@note_bot')
  }
  callback(null, request)
}
```

これだけです。

UserAgent の中身を見て、`Twitterbot` や `facebookexternalhit` が含まれていたら、`/@note` の URL を `/@note_bot` へ変更をします。

手動で Lambda を作るのは面倒くさいので、ServerlessFramework を使います。

```bash
sls create -t aws-nodejs --name cloudfront-url-rewrite
```

### ServerlessFramework 設定

IAM ロールなどが必要なので serverless.yml ファイルに定義します。@edge で使うためには `us-east-1` リージョンへデプロイする必要があるので注意してください。

```yml
service: cloudfront-url-rewrite

provider:
  name: aws
  runtime: nodejs6.10
  region: us-east-1
  memorySize: 128
  timeout: 1
  role: LambdaEdgeRole

functions:
  hello:
    handler: handler.redirect

resources:
  Resources:
    LambdaEdgeRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
                  - edgelambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Policies:
          - PolicyName: ${opt:stage}-serverless-lambdaedge
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - logs:CreateLogGroup
                    - logs:CreateLogStream
                    - logs:PutLogEvents
                    - logs:DescribeLogStreams
                  Resource: 'arn:aws:logs:*:*:*'
                - Effect: "Allow"
                  Action:
                    - "s3:PutObject"
                  Resource:
                    Fn::Join:
                      - ""
                      - - "arn:aws:s3:::"
                        - "Ref" : "ServerlessDeploymentBucket"
```

あとは普通にデプロイすれば OK です。

```bash
sls deploy -v --stage production
```

## CloudFront 設定

次は作成した Lambda@edge を CloudFront へ紐付けます。

「Behaviors」を新規作成し、一番下にある「Lambda Function Associations」に追加します。

- Viewer Request : ARN

Lambda@edge の ARN を設定するのですが、バージョンも含めて指定する必要があるので注意してください。

例 (この場合バージョン6) `arn:aws:lambda:us-east-1:1234:function:cloudfront-url-rewrite-production:6`

## BOT 用の URL を用意

BOT は `/@note_bot` という URL に振り分けられているので Firebase 側で用意してあげます。

```json
{
  "hosting": {
    "rewrites": [
      {
        "source": "/@note_bot/*",
        "function": "note"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
```

### note Function

```typescript
import * as functions from 'firebase-functions';
import DocumentSnapshot = FirebaseFirestore.DocumentSnapshot;
import DocumentData = FirebaseFirestore.DocumentData;
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
const db = admin.firestore()

function buildHtmlWithPost (id: string, noteObj:DocumentData) : string {
  return `<!DOCTYPE html><head>
  <title>${noteObj.title}</title>
  <meta property="og:title" content="${noteObj.title}">
  <meta property="og:image" content="${noteObj.image}">
  <meta property="og:image:width" content="600">
  <meta property="og:image:height" content="600">
  <meta name="twitter:card" content="summary">
  <meta name="twitter:title" content="${noteObj.title}">
  <meta name="twitter:image" content="${noteObj.image}">
  <link rel="canonical" href="/@note/${id}">
  </head><body>
  <script>window.location="/@note/?noteId=${id}";</script>
  </body></html>`
}

export const note = functions.https.onRequest(function(req, res) {
  const path = req.path.split('/')
  const noteId = path[2]
  db.collection('note').doc(noteId).get().then((doc:DocumentSnapshot) : void => {
    const htmlString = buildHtmlWithPost(noteId, doc.data())
    res.status(200).end(htmlString)
  }).catch(err => {
    res.status(500).end(err)
  })
})
```

以上です。

なお、今は ServerlessFramework 自身が Lambda@edge に対応していませんが、対応予定で進んでいるみたいなのでしばらくしたら、もっと簡単にできるかもしれません。