CloudFront+S3なSPAにLambda@EdgeでOGP対応する
吉川@広島です。
案件でCloudFront+S3なSPAに対してOGP対応が必要になってきそうなため、Lambda@Edgeを使った対応について検証しました。
現状、FacebookやTwitterのBotは基本的にクライアントサイドJSを解釈できず、SPA単体でのOGP対応は難しいとされています。OGPメタタグはSSRで返してあげる必要があるため、「UserAgentでBot判定し、その時だけOGPメタタグ入りのHTMLをエッジサーバでレンダリングして返す」というのが基本戦略になります。
SPAをホスティングするCloudFront+S3を作成する
S3バケットを作成する
バケット名だけ入力し、後はデフォルト値で作成します。
そして、本来であればSPA用のHTML/JS/CSSリソースをアップロードするのですが、今回はLambda@Edgeの動作確認ができれば良かったためパスしました(バケットを空にしておく)。「SPAリソースがあるつもり」で進めていきます。
CloudFront Distributionを作成する
Create distributionから、
- Origin domain: 作成したS3バケット
- S3 bucket access: Yes...を選択
- OAI: Create new OAIから作成しそれを選択
- Bucket policy: Yes...を選択
- Viewer protocol policy: Redirect HTTP to HTTPSを選択
の設定で作成します。
Lambda@Edgeを作成する
リージョンをバージニア北部に切り替え、Lambda関数の新規作成をしていきます。
設計図の使用を選択します。「cloudfront」で検索し、cloudfront-modify-response-headerを雛形として使用することにします。選択したら「設定」を押下します。
関数名とロール名を入力します。
- ディストリビューション: 作成したCloudFront Distribution ARNを入力
- CloudFrontイベント: ビューアーリクエストを選択
- Lambda@Edgeへのデプロイを確認: チェックを入れる
としてデプロイします。
以上でLambda関数が作成されるので、次はコードの編集をしていきます。「コードを編集」を押下します。
埋め込みのCloud9コンソールよりコードを編集します。今回は次のようなコードとしました。
const URL_PREFIX = 'https://{SPA_CloudFront_サブドメイン}.cloudfront.net' const BOTS = [ 'Twitterbot', 'facebookexternalhit', 'line-poker', 'Discordbot', 'SkypeUriPreview', 'Slackbot-LinkExpanding', 'PlurkBot', ] const generateContent = ({ title, description, imageURL, mimeType, width, height, url, }) => { console.log( JSON.stringify( { title, description, imageURL, mimeType, width, height, url, }, null, 2 ) ) return ` <!doctype html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" /> <meta content="width=device-width, initial-scale=1.0" name="viewport" /> <title>${title}</title> <meta content="${description}" name="description"> <meta content="${url}" property="og:url" /> <meta content="article" property="og:type" /> <meta content="ja_JP" property="og:locale" /> <meta content="${title}" property="og:title" /> <meta content="${description}" property="og:description" /> <meta content="${width}" property="og:image:width" /> <meta content="${height}" property="og:image:height" /> <meta content="${imageURL}" property="og:image" /> <meta content="${imageURL}" property="og:image:secure_url" /> <meta content="${mimeType}" property="og:image:type" /> <meta content="summary_large_image" property="twitter:card" /> <meta content="@XXXXX" property="twitter:site" /> <meta content="${title}" property="twitter:title" /> <meta content="${description}" property="twitter:description" /> <meta content="${imageURL}" property="twitter:image" /> </head> <body> </body> </html> ` } exports.handler = async (event) => { console.log(JSON.stringify({ event }, null, 2)) const request = event.Records[0].cf.request const path = request.uri const userAgent = request.headers['user-agent'][0].value console.log({ userAgent }) const isBot = BOTS.some((v) => { return userAgent.includes(v) }) console.log({ isBot }) if (isBot) { const url = `${URL_PREFIX}${path}?${request.querystring}` // pathによって画像を出し分ける let body switch (path) { case '/foo': body = generateContent({ title: 'FOO', description: 'FOO Page', imageURL: 'https://{OGP画像_CloudFront_サブドメイン}.cloudfront.net/2400x1260.png', mimeType: 'image/png', width: 1200, height: 630, url, }) break default: body = generateContent({ title: 'OGPタイトル', description: 'OGP説明', imageURL: 'https://{OGP画像_CloudFront_サブドメイン}.cloudfront.net/1200x630.png', mimeType: 'image/png', width: 1200, height: 630, url, }) } return { status: '200', statusDescription: 'OK', headers: { 'content-type': [ { key: 'Content-Type', value: 'text/html', }, ], }, body, } return request }
コードは主に下の記事を参考に、
AmplifyでOGP対応はできない。でもLambda@edgeを使えば大丈夫! | Fixel株式会社
BOTS
一覧はこちらの記事を参考にさせて頂きました。
OGP取得用クローラのユーザーエージェントを、TwitterやLINEなど8サービスで調べてみた - Crieit
コードを編集しデプロイしたら、新規にバージョンを発行します。
CloudFront Behaviorを編集して最新Lambda関数バージョンを反映する
今、Lambda関数バージョンを発行したので、CloudFrontに紐付けるバージョンも更新する必要があります。ARNのバージョン部分を書き換えて更新します。キャプチャはバージョン3と紐付ける例です。
OGP画像をホスティングするCloudFront+S3を作成する
S3バケット、CloudFront Distributionを作成する
SPAホスティングと同じ手順で作成します。
動作確認用の画像をアップロードする
OGPの出し分けを確認するために、画像を2つ用意します。Placehold.jpで1200x630と2400x1260のpng画像を作成し、S3バケットにアップロードしました。
S3バケットのCORS対応(ハマった)
S3バケットのアクセス許可からCross-Origin Resource Sharing (CORS)を編集します。任意のドメインからのアクセスを許可します。
[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "GET" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": [], "MaxAgeSeconds": 3000 } ]
これを行わない場合、後述するSharing Debugger - Facebook for Developersで確認した際、
Provided og:image URL, https://{SUB_DOMAIN}.cloudfront.net/example.png could not be processed as an image because it has an invalid content type
とエラーになり、OGP画像が表示されなかったので注意しましょう。
動作確認
デバッグツール
表示のされ方を確認したいというのもそうですが、特にUserAgent判定を動作確認するために本物のBotに見に行かせたいという事情があります。
を利用すると良いでしょう。
動作確認結果
Facebook、Twitterの両方とも、placehold.jpで作成したOGP画像を表示できました。URLパスによる出し分けも意図通り機能していました。
/fooを確認した場合:
/foo以外を確認した場合:
/fooを確認した場合:
/foo以外を確認した場合:
CloudWatchログは米国リージョンも確認する(ハマった)
上記デバッグツールからのアクセスがCloudWatchログに出力されず首をひねっていました。東京リージョンのCloudWatchを見ていて、手元のMacからのアクセスはログ出力されているのに、Botからのアクセスは出ない、なぜ・・・?と思っていたのですが、バージニア北部リージョンを確認すると出力されていました。
これはLambda@Edgeがアクセス元に近いエッジサーバで実行されるためと考えられます。(当たり前といえばそうですが)FacebookやTwitterのBotは米国で動作しているのでしょう。
参考
- Lambda@Edge で URLパスを書き換える | DevelopersIO
- Lambda@EdgeでSPAのOGPを動的に設定する - Qiita
- Lambda コンソールで Lambda@Edge 関数を作成する - Amazon CloudFront
- AmplifyでOGP対応はできない。でもLambda@edgeを使えば大丈夫! | Fixel株式会社
- FacebookやTwitterの投稿時表示画像(OGP)を確認・修正する方法 | New Standard
- facebook - FB OpenGraph og:image not pulling images (possibly https?) - Stack Overflow
- OGP画像の埋め込みを実装したい(しない) - The curse of λ
- Sharing Debugger - Facebook for Developers
- html - Provided og:image URL, {url to AWS S3} could not be processed as an image because it has an invalid content type - Stack Overflow
- http headers - Facebook says an open graph image has an invalid content type and ignores it - Stack Overflow
- CloudFrontとS3のCORS対応 - Qiita
- GAになったLambda@Edgeを使ってSPAをSSR無しでOGPとかに対応させてみる - Qiita
- OGP取得用クローラのユーザーエージェントを、TwitterやLINEなど8サービスで調べてみた - Crieit
- AWSマネジメントコンソールからのAmazon S3のCORS設定方法がJSON記法になっていました | DevelopersIO
- CloudFrontとS3のCORS対応 - Qiita