Edited at

Firebase で Vue.js(QuasarFramework) の SSR をして Google にインデックスされるようにする

More than 1 year has passed since last update.

Firebase + Vue.js の構成で SSR (ServerSideRendering) をする実例や紹介記事はそれなりに出てきているが、たいていは Nuxt.js を使ったもの。今回は Vue.js をベースにしたフレームワーク QuasarFramework を使って Firebase で SSR する構成例を紹介する。

QuasarFramework v0.17.0 は SSR に対応しているので、それをうまいこと Cloud Functions で動かすようにすれば良い。しかも QuasarFramework の SSR は内部的には vue-server-rendererexpress を使っているだけで、特別なことはしていない。つまり Nuxt.js と同じようなアプローチになる。


今回のデモとソース


QuasarFramework の SSR 対応

QuasarFramework で SSR に対応するには前回書いた記事があるので、それを参照してほしい。


Cloud Functions の作成

Cloud Functions の中に QuasarFramework でビルドした SSR 用のファイルを含める必要がある。そのためビルド先を ./functions 以下に変更する。


quasar.conf.js

QuasarFramework のビルド先を functions/dist に指定。


module.exports = function (ctx) {
return {
build: {
scopeHoisting: true,
vueRouterMode: 'history',
distDir: 'functions/dist', // 追加
}
}
}


functions/index.js

続いて Cloud Functions を作っていく。

const functions = require('firebase-functions');

const server = require('./server/server.js');

exports.server = functions.https.onRequest(server.handler);


functions/server/server.js

これは QuasarFramework を SSR モードでビルドしたときに自動生成される src-ssr/index.js をほぼコピーして持ってきている。変更した点といえばビルドファイルの読み込みパスと express のサーバ起動をやめてアプリケーションを export しているくらい。

const

express = require('express'),
compression = require('compression')

const
ssr = require('../dist/ssr'),
app = express(),
port = process.env.PORT || 3000

const serve = (path, cache) => express.static(ssr.resolveWWW(path), {
maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0
})

// gzip
app.use(compression({ threshold: 0 }))

// serve this with no cache, if built with PWA:
if (ssr.settings.pwa) {
app.use('/service-worker.js', serve('service-worker.js'))
}

// serve "www" folder
app.use('/', serve('.', true))

// this should be last get(), rendering with SSR
app.get('*', (req, res) => {
res.setHeader('Content-Type', 'text/html')
ssr.renderToString({ req, res }, (err, html) => {
console.log('error', err)
if (err) {
if (err.url) {
res.redirect(err.url)
}
else if (err.code === 404) {
res.status(404).send('404 | Page Not Found')
}
else {
// Render Error Page or Redirect
res.status(500).send('500 | Internal Server Error')
if (ssr.settings.debug) {
console.error(`500 on ${req.url}`)
console.error(err)
console.error(err.stack)
}
}
}
else {
res.send(html)
}
})
})

exports.handler = app;


functions の package

QuasarFramework の SSR には色々なパッケージに依存しているので、それを Cloud Functions で使えるようにインストールする。

cd functions

npm install --save @babel/runtime axios compression core-js express \
lru-cache regenerator-runtime vue vue-i18n vue-router vue-server-renderer vuex


デプロイスクリプト

QuasarFramework のビルドで生成される package.json は必要ないため削除する。手動で削除するのが面倒くさいので、ビルドスクリプトを用意する。

#!/usr/bin/env bash

quasar build -m ssr
rm -rf ./functions/dist/package.json
firebase deploy


Firebase の rewrite 設定

あとは Firebase にアクセスした際に Functions へ引き渡すように設定すれば完了。


firebase.json

public を QuasarFramework がビルドする静的ファイルのディレクトリ functions/dist/www を指定。 rewrites でアクセスを server function に渡すように設定。

{

"functions": {
"predeploy": [
"npm --prefix \"$RESOURCE_DIR\" run lint"
]
},
"hosting": {
"public": "functions/dist/www",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"function": "server"
}
]
}
}

これでデプロイが無事完了すれば SSR されるはず。


Google クローラーの確認

Google SearchConsole で fetch してもらった HTML を念の為確認する。SSR しているものと通常の Ajax でレンダリングしたものを分けるために以下のように実装したもので試す。


Post.vue

<template>

<q-page padding>
<qo-menu></qo-menu>
<h1>Posts page</h1>
<h2>SSR contents</h2>
<ul>
<li v-for="(tag, index) in getTags" :key="index">
{{tag.id}}
</li>
</ul>
<h2>Ajax contents</h2>
<ul>
<li v-for="(post, index) in getPosts" :key="index">
{{post.id}}: {{post.title}}
</li>
</ul>
</q-page>
</template>

<script>
import { mapGetters, mapActions } from 'vuex'
import QoMenu from '../components/QoMenu.vue'

export default {
components: {
QoMenu
},
preFetch ({ store, currentRoute, previousRoute, redirect, ssrContext }) {
return store.dispatch('post/fetchTags')
},
computed: {
...mapGetters('post', [
'getTags',
'getPosts'
])
},
methods: {
...mapActions('post', [
'fetchZaruPosts'
])
},
mounted () {
this.fetchZaruPosts()
}
}
</script>

<style>
</style>


preFetch で取得したデータは SSR の対象になるが、 mounted などで取得しているデータは SSR はされない。つまり一般的な SPA などと変わらない。

SSR_Firebase_Sample.png

これを Google SearchConsole から手動クロールした結果の HTML を確認する。

URL_検査.png

細かくてわかりにくいが preFetch で取得したデータに関してはちゃんと HTML としてレンダリングされ Google のクローラにも認識されている。Good!


リンクを <a> タグにする

あとは Google クローラーが巡回しやすいように重要な動線を <a> タグを使い href で URL 指定すれば OK 。VueRouter であれば <router-link> を使えば良い。QuasarFramework は <router-link> をラップしたコンポーネントがたくさんあるので同じようにオプションを指定する。

<router-link :to="{ name: 'home' }">Home</router-link>

<q-item link tag="a" :to="{ name: 'Home' }">
<q-item-main>
Home
</q-item-main>
</q-item>

こうすることで以下のようにレンダリングされる。

<a href="/">Home</a>

このようにしておかないと、せっかくクロールされても単体の孤立したページとして認識されてしまうので気をつけたい。


Cloud Functions をキャッシュする

Cloud Functions はしばらくアクセスされないとインスタンスが削除されスリープ状態になる。つまり遅い。SEO やユーザ体験的に SSR したコンテンツの表示が遅いのは困るので、キャッシュさせることにする。幸いなことに Cloud Functions は Firebase Hosting の CDN に対応しているので、単純にキャッシュコントロールを指定すれば良い。キャッシュされては困るようなものは preFetch ではなく通常の Ajax で取得するようにすれば良い。

app.get('*', (req, res) => {

res.setHeader('Content-Type', 'text/html')
res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=600')
// 略
})

これで600秒間のキャッシュが作成され高速にレスポンスされる。最高。