Help us understand the problem. What is going on with this article?

Vue.jsのプリレンダリングで手軽なOGP対応

More than 1 year has passed since last update.

はじめに

プリレンダリングは、Nuxt.js の Generate のようなもので、ビルド時にルートごとの HTML ファイルを生成する仕組みです。去年参加した v-meetup で、おいちゃんさんが「プリレンダリングでもわりと十分なんだよ~」とお話をされていたので、ずっとやろうと思ってました(遅い)。公式ガイドでも小規模な静的ページならプリレンダリングを推奨しているようです。

🐹 SSR vs プリレンダリング (事前描画)

最近チョコチョコ使うようになった Netlify にプリレンダリングの機能がありましたが、まだベータ版なのと、キャッシュされるタイミングは調整できないみたい?
課金すれば外部サービスを利用できるらしく、Vue.js の SPA でどのぐらいシームレスに導入できるのかちょっと気になります。近いうちにホスティングサービスを乗り換えると思うので、また今度試してみようと思います。

そんなわけで、今回は Netlify の機能ではなく「Prerender SPA Plugin」を使用しました。

  • Vue.js 2.5.13
  • Vue CLI 3.8.2
  • Prerender SPA Plugin 3.4.0

プロジェクトのセットアップ

Node.js と yarn は導入しているものとして、Vue CLI3(自己責任Ver)を使用しています。
Vue CLI3 では、対話式にいろいろな機能が選択できるようになったので、適当に選んでプロジェクトを作成します。

yarn global add @vue/cli
vue create my-project

もちろん、Vue Router はオンにします。

Prerender SPA Plugin のインストール

yarn add prerender-spa-plugin

Vue Router の設定

ルーターのモードは「history」にします。他はデフォルトの状態です。

src/router.js
export default new Router({
  mode: 'history', // ← 必須
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      component: About
    }
  ]
})

vue.config.js の設定

プロジェクトルートに vue.config.js を作成して、プラグインを追加します。
基本的な設定方法は、次のような感じです。

vue.config.js
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')

module.exports = {
  configureWebpack: config => {
    // プロダクトモードでのみ追加
    if (process.env.NODE_ENV === 'production') {
      return {
        plugins: [
          // ★ Prerender SPA Plugin を登録
          new PrerenderSPAPlugin({
            // 出力先 dist や www など
            staticDir: path.join(__dirname, 'dist'),
            // 生成したいページ
            routes: [ '/', '/about' ]
          })
        ]
      }
    }
  }
}

ビルドする

yarn build

で OK!
この設定では、プロジェクトルートの「dist」ディレクトリの中にファイルが作成されます。

dist/
 ├ about/
 │  └ index.html
 ├ css/
 ├ img/
 ├ js/
 └ index.html

パラメータのあるページ

次のように、パラメータのある動的ルートの場合…

src/router.js
export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/note/:id',
      name: 'note',
      component: Note,
      props: route => ({ id: Number(route.params.id) })
    }
  ]
})

別個に登録する必要があるみたいです。こんなかんじ(´•ω•`)?

vue.config.js
new PrerenderSPAPlugin({
  // 出力先 dist や www など
  staticDir: path.join(__dirname, 'dist'),
  // 生成したいページ
  routes: [ '/', '/about', '/note/1', '/note/2', '/note/3' ]
})

ちょっとつらいので、数が多ければ for とかで動的に作成するのがよさそう。
といっても、ページ数に比例してビルドに時間がかかるようなので、あまりにもページ数の多いサイトでは素直に SSR した方がいいです。

ページによってタイトルやメタ情報を変更

Vue.js で出力された HTML をそのまま拾ってくれるため、このへんのメタ情報を更新するプラグインを使えば簡単に OGP などの対応ができます。

🐹 vue-meta

フックを使って自分で更新してもいい。

src/views/Note.vue
export default {
  props: { id: Number },
  created() {
    document.title = `Note:${ this.id }`
  },
  destroyed() {
    document.title = `Default Title`
  }
}

複数の項目を変更するには Mixin かプラグイン作った方がスッキリしますね。

公式の説明では、次のようにpostProcessを使ってテキストベースで変換します。
わりとアナログな感じですね!

vue.config.js
new PrerenderSPAPlugin({
  staticDir: path.join(__dirname, 'dist'),
  routes: [ '/', '/about', '/note/1', '/note/2' ],
  postProcess (renderedRoute) {
    const titles = {
      '/': 'Home',
      '/about': 'Our Story',
      '/note/1': 'Note1',
      '/note/2': 'Note2'
    }
    renderedRoute.html = renderedRoute.html.replace(
      /<title>[^<]*<\/title>/i,
      '<title>' + titles[renderedRoute.route] + '</title>'
    )
    return renderedRoute
  }
})

個人的に、プリレンダリングの目的は OGP の対応のみなので、低コストなものだったりパラメータを使って動的にしたいものは、コンポーネントやルーターのフックでやってもいいかな~と思いました。

非同期処理

初期データのロードなど非同期処理を待つ場合は、カスタムイベントを明示的に発火するまでコンテンツの取得を遅延できます。

vue.config.js
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer

module.exports = {
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      return {
        plugins: [
          new PrerenderSPAPlugin({
            staticDir: path.join(__dirname, 'dist'),
            routes: ['/', '/about'],
            renderer: new Renderer({
              // document.dispatchEvent(new Event('custom-render-trigger'))
              // されるまで読み込みを待つ
              renderAfterDocumentEvent: 'custom-render-trigger'
            })
          })
        ]
      }
    }
  }
}

たとえば、コンポーネントのルーターフックで非同期にデータを読み込みます。

src/views/Note.vue
beforeRouteEnter(to, from, next) {
  const id = Number(to.params.id)
  axios.get(`/note/${id}.md`).then(res => {
    next(vm => {
      vm.content = marked(res.data)
    })
  })
}

content をバインドした DOM の更新を待つため、グローバルのルーターフック afterEachnextTick を使って custom-render-trigger を発火します。

src/router.js
// すべてのルート遷移後
router.afterEach((to, from, next) => {
  Vue.nextTick(() => {
    document.dispatchEvent(new Event('custom-render-trigger'))
  })
})

これで、非同期に取得したデータを使って OGP を生成したりできます👍

ついでに Netlify へのデプロイ

「dist」ディレクトリにファイルを生成しているので、リポジトリを作成して次のような設定でデプロイできます。

項目
Build command yarn build
Publish directory dist

おわりに

めっちゃお手軽です(๑'ᴗ'๑)❤❤ Nuxt も最近使っていたんですけど、私の PC がレガシーなためか、ホットリロードがちょっともっさりするのが気になっていたので、静的ページならプリレンダリングでいいなと思いました!
あと、Netlify CLI 使ったことがないので今度試す。

mio3io
今後なに書くか考え中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away