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

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

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

<img width="400" alt="スクリーンショット 2019-10-27 6.13.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418007/6d75e587-f96d-aaca-7f17-300147c02394.png">

#URL構成

| URL                 |vue ファイル             |       ページ内容   |
|:-----------------   |:------               |:------------------|
| /blog               |/pages/blog/index.vue |記事一覧ページ<br>（ページネーションを設置したい）|
| /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 ＋ 動的なルーティング](https://ja.nuxtjs.org/api/configuration-generate/#routes) の部分なので `nuxt.config.js` の `generate.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.js` の `modules` セクションから読み込めばフックが有効化される。

後は `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.$