72
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Firebase + SPA で SSR なしに OGP 対応

Last updated at Posted at 2018-03-10

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 対応をすることができました。

72
47
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?