Nuxt.jsにページネーションを設置する~脱・Vuejs-paginate編

  • 公開日アイコン
Nuxt.jsにページネーションを設置する~脱・Vuejs-paginate編 メイン画像

Ad Area

Nuxt.jsにページネーションを設置する~Vuejs-paginate編では、Vuejs-paginateパッケージを使ってページネーションを設置した。ただ、パッケージの最終更新4年前と古めであること*、あくまで同ページ内のページ遷移であるためページ分割はされておらず、ブラウザバックすると1ページ目に戻ってしまう点が気になっていた。

* とはいえ、2022年6月現在でも週に36000ダウンロードされている。

そこで、いろいろなサイトやリポジトリを参考にしつつ、ページ分割されたページネーションを設置することにした。Vuejs-paginateの時の記述を生かしつつ、参考ページを基にPaginationコンポーネントを作成するという方法である。

環境

Nuxt.js 2.15.8
nuxt/content1.15.1
Node.js14.16.1

一覧ページ

(以下、コード内の...は省略を表す)

/pages/post/index.vue
<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にする。

/pages/post/category/_category.vue
    ...
    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)。

/pages/post/tag/_tag.vue
    ...
    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部分

/components/Pagination.vue
<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ページ以降を省略して最終ページを出力。先頭ページのため、「前に戻る」リンクはなし
2ページ目
「前に戻る」リンクが表示される
5ページ目
1ページ目の後ろに省略が表示される。±2ページ分(3~7ページ)出力、7ページ以降省略
最終ページ
現在ページから-2ページ分(8・9ページ)出力、それ以前のページは1ページ目のみ表示して省略。最終ページのため「次に進む」リンクはなし

js部分

/components/Pagination.vue
<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ブログのリポジトリ(オープンソース)ではpagestype: 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でルートの拡張をする必要がある。

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にした(カテゴリーとタグの方は重複扱いされなかった。なぜだろう)。

参考ページ

Ad Area