この記事は最終更新日から 2 年以上が経過しており、内容が古くなっている可能性があります。
Nuxt.js に Vuejs-paginate パッケージなしでページネーションを追加し、ブラウザバックにも対応する方法について。
Nuxt.js にページネーションを設置する~ Vuejs-paginate 編では、Vuejs-paginate パッケージを使ってページネーションを設置した。ただ、パッケージの最終更新 4 年前と古めであること*、あくまで同ページ内のページ遷移になるためページ分割はされず、ブラウザバックすると 1 ページ目に戻ってしまう点が気になっていた。
* とはいえ、2022年6月現在でも週に36000ダウンロードされている。
そこで、Vuejs-paginateの時の記述を生かしつつ、いろいろなサイトやリポジトリを参考に、ページ分割されたPaginationコンポーネントを作成することにした。
環境
Nuxt.js | 2.15.8 |
nuxt/content | 1.15.1 |
Node.js | 14.16.1 |
一覧ページ
(以下、コード内の...
は省略を表す)
<template>
<main class="l-main min-h-screen pt-6 pb-8" role="main">
...
<CardList :posts="posts" />
<Pagination
v-if="getPageCount > 1"
:pages="getPageCount"
:current="currentPage"
:category="selectedCategory"
:tag="selectedTag"
/>
...
</main>
</template>
<script>
export default {
data() {
return {
items: "",
parPage: "",
currentPage: "",
};
},
computed: {
posts: function () {
const end = this.currentPage * this.parPage;
const start = end - this.parPage;
return this.items.slice(start, end);
},
getPageCount: function () {
return Math.ceil(this.items.length / this.parPage);
},
},
async asyncData({ $content, params }) {
const items = await $content("post")
.where({ draft: false })
.sortBy("published", "desc")
.fetch();
const parPage = 12;
const currentPage = parseInt(params.p) || 1;
const selectedCategory = undefined;
const selectedTag = undefined;
return {
items,
parPage,
currentPage,
selectedCategory,
selectedTag,
};
},
};
</script>
Vuejs-paginate の設定のうち、methods
部分は不要なので削除。asyncData
で記事データを取得する際の第 2 引数をparams
として追加し、データを取得する。
currentPage
:現在のページ。1 またはp
(ページ番号。p
は後ににも出てくる)selectedCategory
:記事が属するカテゴリー。無しの場合はundefined
selectedTag
:記事が属するタグ。無しの場合はundefined
selectedCategory
selectedTag
はページネーションのページリンクを出力する時に必要な情報である。この値によって、全体記事一覧・カテゴリー記事一覧・タグ記事一覧の判別を行う。
全体記事一覧では、上記の通りselectedCategory
selectedTag
ともにundefined
にする。
...
async asyncData ({ $content, params }) {
const categories = await $content('category')
.where({ slug: { $contains: params.category } })
.limit(1)
.fetch()
const category = categories.length > 0 ? categories[0] : {}
const items = await $content('post', params.slug)
.where({ category: { $contains: category.name }, draft: false })
.sortBy('published', 'desc')
.fetch()
const parPage = 12;
const currentPage = parseInt(params.p) || 1;
const selectedCategory = category;
const selectedTag = undefined;
}
...
カテゴリーの一覧ページでは、ページ情報のほかにカテゴリー情報を取得する。selectedCategory
を変数category
とする(selectedTag
の値はundefined
)。
...
async asyncData ({ $content, params }) {
const tags = await $content('tag')
.where({ slug: { $contains: params.tag } })
.limit(1)
.fetch()
const tag = tags.length > 0 ? tags[0] : {}
const items = await $content('post', params.slug)
.where({ tags: { $contains: tag.name }, draft: false })
.sortBy('published', 'desc')
.fetch()
const parPage = 12;
const currentPage = parseInt(params.p) || 1;
const selectedCategory = undefined;
const selectedTag = tag;
}
...
タグの一覧ページでは、ページ情報のほかにタグの情報を取得を追加する。selectedTag
を変数tag
とする(selectedCategory
の値はundefined
)。
ページネーションコンポーネント
html 部分
<template>
<div class="c-pagination-wrap mt-12">
<ul class="c-pagination flex justify-center">
<li v-if="current > 1" class="c-pagination-btn c-pagination-prev">
<nuxt-link :to="getPath(current - 1)" class="c-pagination-btn__link"
>←</nuxt-link
>
</li>
<li v-if="3 < current" class="c-pagination-item">
<nuxt-link :to="getPath(1)" class="c-pagination-item__link"
>1</nuxt-link
>
</li>
<li v-if="4 < current" class="c-pagination-omit">
<span>...</span>
</li>
<li
v-for="p in pages"
:key="p"
v-show="current - 2 <= p && p <= current + 2"
class="c-pagination-item"
:class="{ active: current === p }"
>
<nuxt-link :to="getPath(p)" class="c-pagination-item__link"
>{{ p }}</nuxt-link
>
</li>
<li v-if="current + 3 < pages" class="c-pagination-omit">
<span>...</span>
</li>
<li v-if="current + 2 < pages" class="c-pagination-item">
<nuxt-link :to="getPath(pages)" class="c-pagination-item__link"
>{{ pages }}</nuxt-link
>
</li>
<li v-if="current < pages" class="c-pagination-btn c-pagination-next">
<nuxt-link :to="getPath(current + 1)" class="c-pagination-btn__link"
>→</nuxt-link
>
</li>
</ul>
</div>
</template>
現在ページの位置によって、ページネーションに出力する項目を変える。
v-if="current > 1
:現在のページが 2 以上の場合、「前に戻る」リンクを出力v-if="3 < current
:現在のページが 4 以上の場合、1 ページ目を出力v-if="4 < current
:現在のページが 5 以上の場合、1 ページ以降に省略...
を出力v-for="p in pages" :key="p"
:pages
の配列をp
に入れ、v-for
で回してページ番号とリンクを出力v-show="current - 2 <= p && p <= current + 2"
:現在のページ-2 以上かつ現在のページ+ 2 以下の条件を満たす値の場合、ページ番号とリンクを出力:class="{ active: current === p }"
現在のページがp
に等しい場合、class にactive
を付与する- 総ページが現在のページ+ 3 より大きい場合、総ページ直前の省略
...
を出力 - 総ページが現在のページ+ 4 より大きい場合、総ページ数=最終ページ数とそのリンクを出力
- 現在のページが総ページ数より小さい場合、「次に進む」リンクを出力
総ページが 10 の場合は以下のような表示になる。
現在ページから 2 ページ分(2・3 ページ)、4 ページ以降を省略して最終ページを出力。先頭ページのため、「前に戻る」リンクはなし
「前に戻る」リンクが表示される
1 ページ目の後ろに省略が表示される。±2 ページ分(3 ~ 7 ページ)出力、7 ページ以降省略
現在ページから-2 ページ分(8・9 ページ)出力、それ以前のページは 1 ページ目のみ表示して省略。最終ページのため「次に進む」リンクはなし
js 部分
<script>
export default {
props: {
pages: {
type: Number,
required: false,
},
current: {
type: Number,
required: true,
},
category: {
type: Object,
required: false,
default: undefined,
},
tag: {
type: Object,
required: false,
default: undefined,
},
},
data() {
return {
p: "",
};
},
methods: {
getPath(p) {
if (this.category !== undefined) {
if (p === 1) {
return `/post/category/${this.category.slug}/`;
} else {
return `/post/category/${this.category.slug}/page/${p}/`;
}
} else if (this.tag !== undefined) {
if (p === 1) {
return `/post/tag/${this.tag.slug}/`;
} else {
return `/post/tag/${this.tag.slug}/page/${p}/`;
}
} else {
if (p === 1) {
return `/post/`;
} else {
return `/post/page/${p}/`;
}
}
},
},
};
</script>
それぞれの index.vue からpages
(総ページ)current
(現在のページ数)category
(カテゴリー)tag
(タグ)の値をprops
で渡す。
microCMS ブログのリポジトリ(オープンソース)ではpages
がtype: Array
となっていたが、当ブログはNumber
にしないと値が通らなかった。computed
で算出した値だからだろうか(この辺はよく理解できていない)。
methods
でパスの URL を指定する関数を作成する。
category
tag
の値で、記事全体・カテゴリー・タグのうちどの一覧かを識別し、パスを出し分ける。- 但し、一覧トップ
/post/
と 1 ページ目/post/page/1/
が共存すると、Google から重複コンテンツと見なされるので、当ブログでは 1 ページを/post/
に統合することにした(/post/page/1/
を生かす場合は、/post/
の方を/post/page/1/
へリダイレクトする必要があるだろう)。
ルーティング
Nuxt.js は、pages
ディレクトリの vue ファイルの構造に従って自動的にページを生成する(ファイルシステムルーティング)。しかし、ここで設定した/post/page/number/
は vue ファイルによって生成されるページではないため、nuxt.config.js でルートの拡張をする必要がある。
export default {
...
router: {
extendRoutes(routes, resolve) {
routes.push({
path: '/post/page/:p',
component: resolve(__dirname, 'pages/post/index.vue'),
name: 'archive',
});
routes.push({
path: '/post/category/:category/page/:p',
component: resolve(__dirname, 'pages/post/category/_category.vue'),
name: 'category',
});
routes.push({
path: '/post/tag/:tag/page/:p',
component: resolve(__dirname, 'pages/post/tag/_tag.vue'),
name: 'tag',
});
},
},
...
}
ルート拡張にはextendRoutes
オプションを使う。
path
:追加するパスを指定。ページ番号p
component
:ページを表示するときに使うコンポーネントファイルname
:ルートの識別名(オプション)。
path: '/post/page/:p'
の識別を post
もしくはindex
としたら、
WARN [vue-router] Duplicate named routes definition: { name: "post", path: "/post/page/:p" }
名前が重複していると警告が出たのでarchive
にした(カテゴリーとタグの方は重複扱いされなかった。なぜだろう)。
参考ページ
- 「vuejs-paginate」を使ってページネーションを実装する | カバの樹
- ブラウザバックしてもページネーションを維持させる方法 | カバの樹
- Nuxt で独自のルーティングを追加・拡張する - V がいる日々
- How to add Pagination to @nuxt/content - ZEMNA.NET nuxt/content で前後 ○ 件のページを出力するタイプのページネーションの紹介
- Nuxt の Jamstack 構成におけるページングの実装 | microCMS ブログ
- microcmsio/microcms-blog: microCMS official blog(オープンソース)