この記事は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 に対応していませんが、対応予定で進んでいるみたいなのでしばらくしたら、もっと簡単にできるかもしれません。