SPA で構築すると気になるのは SEO と OGP です。SEO に関しては Google のクローラは JS を解釈してくれるようになったので fetch as Google で確認する限りインデックスされるようです(順位に影響があるかどうかは不明)。OGP に関しては現状、 facebook / twitter のクローラは JS を解釈してくれないので、サーバから返却される HTML に <meta property="og:title" content="title">
のようなタグを埋め込んでおく必要があります。
SPA OGP 対策として一番最初に語られるのが SSR (サーバサイドレンダリング) だと思いますが、OGP のためだけに SSR するのは面倒なので、Firebase Hosting + Functions を使ってなんとかして見る方法を試してみました。そのメモです。
注意
今回の対応方法は Firebase のみで構築をするパターンです。これには弱点が幾つかあります。
- リダイレクト処理が入るので、最初のレスポンスが微妙
- Google のインデックスがリダイレクトが入るため評価されるか微妙
これを回避するためには CloudFront + Lambda@edge を併用するパターンもあります。こちらも対応方法を書いたので参考にしてみてください。
Firebase + SPA + CloudFront + Lambda で SSR なしに OGP 対応
Hosting から Functions へ
Firebase Hosting では .firebase
ファイルにルールを記載すると、特定 URL 時に Functions へ処理を受け渡すことができます。
{
"hosting": {
"rewrites": [
{
"source": "/@note/*",
"function": "note"
}
]
}
}
この例だと /@note/piyo
のような URL にアクセスすると note Function が起動します。
Functions で OGP 含む HTML
次にリクエストを受け取った note FUnction で、OGP のタグを含んだ HTML を返却するようにします。
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)
})
})
この例では、URL に含まれている ID を元に firestore へデータを取得して適宜 HTML に埋め込んでいます。
ポイントは <meta>
タグのみを記載し、実際のコンテンツは window.location
でリダイレクトしているところです。こうすると、 facebook / twitter などはリダイレクトされず、通常ユーザは /@note/?noteID=xxx
へリダイレクトされます。
このままだと正規の URL /@note/xxx
が /@note/?noteID=xxx
になってしまうため HTML5 history replace を使って URL を書き換えてあげます。vue-router を使っている例はこちら。 $route.query
でクエリパラメータがあった場合は正規 URL に変更してあげるような処理です。
created () {
if (this.$route.query.noteId) {
this.$router.replace('/@note/' + this.$route.query.noteId)
}
}
以上で、 Firebase のみで OGP 対応をすることができました。