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

この記事は最終更新日から 1 年以上が経過しており、内容が古くなっている可能性があります。

Nuxt.js に Vuejs-paginate パッケージなしでページネーションを追加し、ブラウザバックにも対応する方法について。

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

一覧ページ

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

<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 ページ以降を省略して最終ページを出力。先頭ページのため、「前に戻る」リンクはなし

2ページ目

「前に戻る」リンクが表示される

5ページ目

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

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

参考ページ

Suzunatsu

このブログについて

コーディングやWeb関連技術の記事と、買い物など日々のメモから成り立っています。 →少しだけ詳しく

この記事がお役に立ちましたら、サポートをお願いします。

広告