2020年2月16日に投稿

【自作ブログ】nuxt generateで動的ルーティングも含める方法

前回は記事の詳細画面を作成しました。
しかしNetlifyにデプロイしてみると記事の詳細ページが表示されないことがわかりました。
今回はこの原因と対策について説明していきます。

なぜダメだったのか?

なぜ表示されなかったのでしょうか。
それはNetlifyにデプロイする際に実行しているnpm run generateの処理が深く関係しています。

nuxt generateは動的ルーティングを無視する

npm run generateを実行した時pages/posts/_slugのような動的に決定されるルーティングは全て無視されます。
考えてみれば当然のことで、_slugの部分は「contentfulから取得した記事のslugの値になります」なんてことをNuxt.jsが知っているわけがないのです。

どうするか?

解決策は単純です。「知らないのなら教えてあげればいい」のです。
nuxt generateが実行される時に動的生成されるルートを全て教えてあげましょう。

generate時のルートを定義する

動的なルーティングを含ませる

解決策はわかったので早速修正していきましょう。
公式の方法を参考にしながらnuxt.config.jsを修正していきます。
generateの中にroutes関数を定義し、動的ルートの配列を返すように実装していきます。

nuxt.config.js

require('dotenv').config()
const sdkClient = require('./plugins/contentful').default // 追加

export default {
  
  // 〜中略〜
  
  generate: {
    routes () {
      return Promise.all([
        sdkClient.getEntries({
          content_type: 'blogPost'
        })
      ]).then(([posts]) => {
        return [
          ...posts.items.map((post) => {
            return { route: `posts/${post.fields.slug}`, payload: post }
          })
        ]
      })
    }
  },
  
  // 〜中略〜
  
}

routes関数の中では、まずContentfulにリクエストを送って全ての記事のデータを取得しています。
そこから動的なルートの配列を生成して返すようになっています。


しかしrouteの他にpayloadも一緒に返しています。これはなんでしょうか?


payloadgenerateによる生成時間を退縮するテクニックとしてAPIに記載されています。
APIに記載されている通りroutes関数でルーティングを生成する時に、必要なデータ以外は破棄するようになっています。
つまり各記事の静的なページを生成する際に、もう一度Contentfulにリクエストを送ってデータを取得しなければならないのです。
pages/posts/_slug.vueコンテキストpayloadとして記事のデータを渡してあげる事によって、この二度手間を解決できるのです。

各ページでpayloadからデータを取得する

では各記事の詳細ページのコンテキストに渡されたpayloadから記事のデータを取得するように修正していきましょう。

pages/posts/_slug.vue

<template>
  
  <!-- 〜中略〜 -->
  
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['posts'])
    // 削除〜ここから〜
    // post () {
    //   return this.posts.find(
    //     post => post.fields.slug === this.$route.params.slug
    //   )
    // }
    // 削除〜ここまで〜
  },
  // 追加〜ここから〜
  async asyncData ({ payload, store, params, error }) {
    const post = payload || await store.state.posts.find(post => post.fields.slug === params.slug)
    if (post) {
      return { post }
    } else {
      return error({ statusCode: '404', message: 'お探しのページは見つかりませんでした' })
    }
  }
  // 追加〜ここまで〜
}
</script>

<style>
// ...
</style>

これでpayloadにデータが入っている時、つまりgenerate時はpayloadから情報を取得するようになりました。
それ以外の時は今まで通りstateから対象のpostを探して表示します。

それでもmiddlewareは止まらない

contextpayloadにデータを渡す事によって、npm run generate実行時にはpayloadを参照すれば記事のデータを取得できるようになりました。


しかし、それだけでは不十分なのです。


実はnpm run generateコマンドで静的ファイルをgenerateするとき、1ページ生成する毎にstoreの内容がリセットされるのです。
前回、私たちはmiddlewareを使ってstore.state.postsが空のときは必ずレンダリング前にContentfulにリクエストを送ってデータを取得するように実装してしまいました。
これでは、いくらgenerate時にpayloadへデータを渡していてもmiddlewareが毎回働いてContentfulへのリクエストが送られてしまいます。

実際にgenerate時の動きを確かめてみましょう。

ログを仕込んで確認してみる

まずはmiddleware/getPosts.jsにログを仕込みます。

middleware/getPosts.js

export default async ({ store, payload }) => {
  console.log('middleware is called.') // 常に出力
  if (!store.state.posts.length) {
    console.log('store.posts is empty...') // postsが空のときだけ出力
    if (payload) {
      console.log('payload is : ' + payload.fields.title) // payloadにデータが入っていれば出力
    }
    await store.dispatch('getPosts')
  }
}

次にローカルでnpm run generateを実行してコンソールログを確認してみましょう。

$ npm run generate
  .
  .
  .
middleware is called.                                                                13:41:08
store.posts is empty...                                                              13:41:08
middleware is called.                                                                13:41:08
store.posts is empty...                                                              13:41:08
payload is : これはテスト2です                                                      13:41:08
middleware is called.                                                                13:41:08
store.posts is empty...                                                              13:41:08
payload is : テストの記事                                                            13:41:08
✔ Generated /                                                                        13:41:09
✔ Generated posts/test2                                                              13:41:09
✔ Generated posts/test                                                               13:41:09

ログの結果から分かる通り、store.postsは毎回空っぽです。
そしてpayloadにデータが入っているにもかかわらずstore.dispatch('getPosts')してしまっている事もわかりました。

payloadにデータがある時はdispatchしないようにする

では必要なとき以外はContentfulへのリクエストが発生しないように修正していきましょう。

middleware/getPosts.js

export default async ({ store, payload }) => {
  if (!store.state.posts.length && !payload) {
    await store.dispatch('getPosts')
  }
}

これで必要なときだけContentfulへのリクエストが送信されるようになりました。

Netlifyにデプロイして確認する

最後にNetlifyにデプロイして確認してみましょう。

$ git add -A
$ git commit -m "記事詳細画面が正常に表示されるように修正"
$ git checkout master
$ git merge develop
$ git push origin master

Netlifyが発行してくれたURLを開いて詳細ページが表示できることが確認できれば完了です。

終わりに

お疲れ様でした。ようやく記事の詳細ページを正しく実装することができました。
ブログサイトの作成を通じて、Nuxt.jsの仕組みがどのようになっているかを少しだけ垣間見れたような気がしませんか?
次回は記事に関連付ける「カテゴリー」の機能を実装していこうと思います。

それではまた次回。