この記事はFirebase + SPA で SSR なしに OGP 対応の改善版です。今回は CloudFront + Lambda@edge を使って Facebook / Twitter BOT の場合のみ別の URL へ振り分けてあげることで OGP に対応しようという試みです。
Lambda@edge
Lambda@edge を使えば CloudFront へのリクエストの際に細かい制御をすることができます。今回は UserAgent を見て条件に一致したら OGP 用の URL へ変更するようにします。
'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 を使います。
sls create -t aws-nodejs --name cloudfront-url-rewrite
ServerlessFramework 設定
IAM ロールなどが必要なので serverless.yml ファイルに定義します。@edge で使うためには us-east-1 リージョンへデプロイする必要があるので注意してください。
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 です。
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 側で用意してあげます。
{
  "hosting": {
    "rewrites": [
      {
        "source": "/@note_bot/*",
        "function": "note"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}
note Function
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 に対応していませんが、対応予定で進んでいるみたいなのでしばらくしたら、もっと簡単にできるかもしれません。
