83
77

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 3 years have passed since last update.

[Nuxt.js] 完全静的なサイトでページネーションを上手いこと実装する

Last updated at Posted at 2019-10-26

※デザインやマークアップではなく、ルーティングの話です

Nuxt.js と静的サイトホスティング(Netlify, Firebase 等)の組み合わせでサイトを構築するシーンが比較的増えてきたが、headlessCMSを使用してブログサイトを制作した際に、ページネーションの実装に少々悩んだ。

スクリーンショット 2019-10-27 6.13.23.png

#URL構成

URL vue ファイル ページ内容
/blog /pages/blog/index.vue 記事一覧ページ
(ページネーションを設置したい)
/blog/article-slug /pages/blog/_slug.vue 記事ページ

を前提とする。

#よくある方法

例えば2ページ目を /blog?p=2 とするクエリ方式が考えられる。
generate したサイトの場合は /blog/index.html (/pages/blog/index.vue) でクエリの数字(今何ページ目か)を受け取り、そのページに表示すべき記事一覧を取得する。
###問題点
その記事一覧を取得する方法は2種類ある。

  1. created フック等でクライアントから API サーバーに都度問い合わせる
  2. asyncData フック等であらかじめ全記事を取得しておいて store や data に格納して generate 、クライアントサイドで必要な記事を取り出す。

前者は

  • 記事一覧ページのアクセス数と同じ回数 API 問い合わせが走ってしまう
  • クライアントがアクセスしてから API に問い合わせるので、ローディングに時間がかかる(Lazy Load といえば聞こえはいいが)

後者は

  • 全記事を内包するので記事数が多いとページが重くなる。

もしこれらを問題とするのであれば(ココ重要)、なんとか上手いことクリアできる方法を探したい。
つまり・・・

#やりたいこと

  • nuxt generate で構築しているサイト(ブログなど)にページネーションをつけたい
  • 完全静的構成にしたい(クライント・API サーバー間は一切通信しない)
  • そのページに必要なデータのみ内包したい

#解決法:ページごとに html ファイルを生成させる
/blog/page/2 のようにページごとに html ファイルを作らせると良い。
すなわち pages/blog/page/_id.vue を用意し、asyncData フック等で route.params.id を取得すればページ番号が取得できるので、その数字をもとに API サーバーからページに必要なだけの記事情報を取得する。

pages/blog/page/_id.vue
<script>
export default {
 async asyncData({route}){
  const pageNumber = parseInt(route.params.id) 

  //ここでAPIサーバーから記事情報を取得する処理
 }
}
</script>

generate + 動的なルーティング の部分なので nuxt.config.jsgenerate.routes にページ数分のルートを追記するのも忘れないようにする。
必要なページ数は API に問い合わせて返ってくるならばそれを使えばいいし、なければ Math.ceil(総記事数/1ページの表示数) なんかで割り出す。

こうすることでページ数分の html が generate され、やりたいことの条件を満たせる。

###ちなみに
この解決法はURL構成で /blog/page/2 のように page ディレクトリを挟んでいるが、 /blog/2 (pages/blog/_id.vue) としてしまうと、記事ページのスラッグ( pages/blog/_slug.vue )もあるため、競合してしまう。
同階層に複数の動的ルートが存在するのを避けるため、敢えて一段階層を下げている。
どうしても同階層でやりたい場合は、ルーティングを生成する build:extendRoutes でフックを掛けて、

  • 数字の時はページ番号として pages/blog/_id.vue
  • その他の時は記事ページのスラッグとして pages/blog/_slug.vue

といった条件分岐でルーティングを構成すれば実現可能。(記事ページのスラッグに数字は使えなくなるが)

#効率化

この解決法では、1ページ目( pages/blog/index.vue )と2ページ目以降( pages/blog/page/_id.vue )にほぼ同じ内容を書かなければならない。

pages/blog/page/_id.vue に統一して、ホスティングサーバー側で /blog//blog/page/1リライトするのも一つの手だが、1ページ目はあくまで /blog/page/1 として generate されるので canonical や og:url 等がややこしくなりがち。

そこで先程同様 build:extendRoutes でフックを掛けて、2ページ目以降も pages/blog/index.vue を使って generate するように統一してしまうと良い。
(あくまでテンプレートとして使うだけなので、html は /blog/page/2/index.html としてgenerateされる。)

hook.js
module.exports = function() {
 this.nuxt.hook('build:extendRoutes', routes => {
   const blogPages = {
     path: '/blog/page/:id',
     component: 'pages/blog/index.vue',
     name: 'blog-page-id'
   }
   routes.unshift(blogPages)
 })
}

こんな感じの js を作成し、nuxt.config.jsmodules セクションから読み込めばフックが有効化される。

後は pages/blog/index.vue の asyncData フック等でページ番号( route.params.id )をもとに API サーバーから記事情報を取得する。
1ページ目の route.params.id は undefined なので typeof とかでバリデーション・条件分岐する。

pages/blog/index.vue
<script>
export default {
 async asyncData({route}){
  let pageNumber = 1
  if (typeof route.params.id !== 'undefined') {
    pageNumber = parseInt(route.params.id)
  }

  //ここでAPIサーバーから記事情報を取得する処理
 }
}
</script>

こうして一つの vue ファイルに統一すると効率化できると同時にヒューマンエラーも少なくなる。

#☕
コーディングというよりアイデアですが、何かの助けになれば幸いです。
$Thank$ $you.$

83
77
0

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
83
77

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?