※デザインやマークアップではなく、ルーティングの話です
Nuxt.js と静的サイトホスティング(Netlify, Firebase 等)の組み合わせでサイトを構築するシーンが比較的増えてきたが、headlessCMSを使用してブログサイトを制作した際に、ページネーションの実装に少々悩んだ。
#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種類ある。
- created フック等でクライアントから API サーバーに都度問い合わせる
- 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 サーバーからページに必要なだけの記事情報を取得する。
<script>
export default {
async asyncData({route}){
const pageNumber = parseInt(route.params.id)
//ここでAPIサーバーから記事情報を取得する処理
}
}
</script>
generate + 動的なルーティング の部分なので 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される。)
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 とかでバリデーション・条件分岐する。
<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.$