microCMS

NuxtのJamstack構成におけるページングの実装

柴田 和祈

この記事は公開後、1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Nuxt.jsでJamstackなサイトを構築する際にハマりがちなページングについて解説します。
こちらの記事のコードにページング機能を追加していきたいと思います。

microCMS + NuxtでJamstackブログを作ってみよう
https://microcms.io/blog/microcms-nuxt-jamstack-blog

一覧画面のページング

前回の記事では以下のような構成で記事の一覧画面と詳細画面を作成しました。

========
pages/
 index.vue
 _slug/
  index.vue
========

ページング時は/page/2のようにアクセスしてもらうため、pages/page/_p/index.vueのように新しくページング用テンプレートを用意したくなりますが、ページングのUIレイアウトは記事一覧画面と基本的には同じです。
なので、テンプレートとしては記事一覧画面(pages/index.vue)をページング画面でも利用する形で開発を進めます。
pages/index.vueに修正を加えていきます。

// pages/index.vue

<template>
  <ul>
    <li v-for="content in contents" :key="content.id">
      <nuxt-link :to="`/${content.id}`">{{ content.title }}</nuxt-link>
    </li>
  </ul>
</template>
<script>
import axios from 'axios'
export default {
  async asyncData({ params }) {
    const page = params.p || '1'
    const limit = 10
    const { data } = await axios.get(
      `https://your-service-id.microcms.io/api/v1/blog?limit=${limit}&offset=${(page - 1) * limit}`,
      { headers: { 'X-MICROCMS-API-KEY': 'your-api-key' } }
    )
    return data
  }
}
</script>

URLのparams経由でページ情報を受け取り、microCMSのリクエストパラメータに含めることでページングを行います。

次に、nuxt.config.jsにてページング用のルーティングを設定していきます。

// nuxt.config.js

export default {
  // 略
  router: {
    extendRoutes(routes, resolve) {
      routes.push({
        path: '/page/:p',
        component: resolve(__dirname, 'pages/index.vue'),
        name: 'page',
      })
    },
  },
}

上記のように設定することで、URLから/page/:pにアクセスしてきた場合はpages/index.vueがテンプレートとして使用されます。
:pにあたる部分がpages/index.vueasyncData()において、params.pで取得可能です。
この時点でnpm run devによる開発環境上では、/page/2とアクセスすると2ページ目が表示されるようになっています。

加えて、Jamstack構成ではページング用のHTMLも事前に生成しておく必要があります。
こちらもnuxt.config.jsで設定します。

// nuxt.config.js

export default {
  // 略
  generate: {
    async routes() {
      const limit = 10
      const range = (start, end) =>
        [...Array(end - start + 1)].map((_, i) => start + i)

      // 一覧のページング
      const pages = await axios
        .get(`https://your-service-id.microcms.io/api/v1/blog?limit=0`, {
          headers: { 'X-MICROCMS-API-KEY': API_KEY },
        })
        .then((res) =>
          range(1, Math.ceil(res.data.totalCount / limit)).map((p) => ({
            route: `/page/${p}`,
          }))
        )
      return pages
    },
  },
}


上記の設定をしておくことで、nuxt generate時にページング用のHTMLが生成されます。
ここではtotalCountだけ取得できればページングのためのルーティングは計算できるので、limit=0として最小限のレスポンスに留めています。
また、以前は動的なルーティングパスに関しては全てこちらに指定してあげる必要がありましたが、最近はNuxt側で内部的なクロールによって動的なルーティングパスを検知してくれるようになったので、最小限の指定で済むようになりました。

以上で、記事一覧のページング用HTMLも静的生成できるようになりました。

応用編:カテゴリー別のページング

記事をカテゴリー別に一覧表示し、ページングも行うパターンを考えてみましょう。
やることは同じですが、カテゴリーによるフィルタリングが必要なので複雑になります。

前提として、カテゴリーAPIを事前に作成し、ブログAPIからコンテンツ参照しておく必要があります。

microCMS + NuxtでJamstackブログを作ってみよう
https://microcms.io/blog/microcms-nuxt-jamstack-blog

上記の記事の「9. カテゴリーを追加する」を参考にしてみてください。

まずはテンプレートです。
先ほどと同じく、記事一覧画面(pages/index.vue)を流用します。

// pages/index.vue

<template>
  <ul>
    <li v-for="content in contents" :key="content.id">
      <nuxt-link :to="`/${content.id}`">{{ content.title }}</nuxt-link>
    </li>
  </ul>
</template>
<script>
import axios from 'axios'
export default {
  async asyncData({ params }) {
    const page = params.p || '1'
    const categoryId = params.categoryId
    const limit = 10
    const { data } = await axios.get(
      `https://your-service-id.microcms.io/api/v1/blog?limit=${limit}${
        categoryId === undefined ? '' : `&filters=category[equals]${categoryId}`
      }&offset=${(page - 1) * limit}`,
      { headers: { 'X-MICROCMS-API-KEY': 'your-api-key' } }
    )
    return data
  }
}
</script>

URLのパラメータからカテゴリーIDとページ情報を受け取り、microCMSにAPIリクエストします。
カテゴリーIDが指定してある場合のみフィルタリングを行い、指定されていない場合は通常の記事一覧のページングを行います。

次に、nuxt.config.jsでルーティングの設定です。

// nuxt.config.js

export default {
  // 略
  router: {
    extendRoutes(routes, resolve) {
      routes.push({
        path: '/page/:p',
        component: resolve(__dirname, 'pages/index.vue'),
        name: 'page',
      });
      routes.push({
        path: '/category/:categoryId/page/:p',
        component: resolve(__dirname, 'pages/index.vue'),
        name: 'category',
      })
    },
  },
}

これで、/category/hoge/page/2とアクセスすればidhogeであるカテゴリーの2ページ目が表示されるようになります。

次に静的生成対応のため、generateの設定を行います。

// nuxt.config.js

export default {
  // 略
  generate: {
    async routes() {
      const limit = 10
      const range = (start, end) =>
        [...Array(end - start + 1)].map((_, i) => start + i)

      // 一覧のページング
      const pages = await axios
        .get(`https://your-service-id.microcms.io/api/v1/blog?limit=0`, {
          headers: { 'X-MICROCMS-API-KEY': 'your-api-key' },
        })
          .then((res) =>
            range(1, Math.ceil(res.data.totalCount / limit)).map((p) => ({
              route: `/page/${p}`,
            }))
          )

      const categories = await axios
        .get(`https://your-service-id.microcms.io/api/v1/categories?fields=id`, {
          headers: { 'X-MICROCMS-API-KEY': 'your-api-key' },
        })
          .then(({ data }) => {
            return data.contents.map((content) => content.id)
          });

      // カテゴリーページのページング
      const categoryPages = await Promise.all(
        categories.map((category) =>
          axios.get(
            `https://your-service-id.microcms.io/api/v1/blog?limit=0&filters=category[equals]${category}`,
            { headers: { 'X-MICROCMS-API-KEY': 'your-api-key' } }
          )
            .then((res) =>
              range(1, Math.ceil(res.data.totalCount / 10)).map((p) => ({
                route: `/category/${category}/page/${p}`,
              })))
      )
      )

      // 2次元配列になってるのでフラットにする
      const flattenCategoryPages = [].concat.apply([], categoryPages)
      return [...pages, ...flattenCategoryPages]
    },
  },
}


ちょっと複雑になってしまいましたが、以下のことを行なっております。

  1. カテゴリーの一覧を取得し、カテゴリーIDの配列を生成する
  2. カテゴリーごとに記事一覧を取得し、件数を取得してルーティングを作成する


以上で、静的生成時にカテゴリー別のページング用HTMLも生成されるようになります。

ページネーションの作成

最後にページングのためのリンクを作成していきます。
色々なパターンのものがあると思いますが、本ブログで提供している下記の形の実装方法を解説していきます。



仕様は次の通りです。

  • 1ページ目と現在のページの前後の2ページと最後のページへのリンクを表示する
  • 例1:全10ページ中1ページ目であれば1, 2, 3 ..., 10ページ目のリンクが表示される
  • 例2:全10ページ中5ページ目であれば1, ..., 3, 4, 5, 6, 7, ..., 10ページ目のリンクが表示される
  • 例3:全10ページ中9ページ目であれば1, ...7, 8, 9, 10ページ目のリンクが表示される



まずはページネーション用のコンポーネントを用意します。
以下の情報をpropsとして渡すと上記のようなページネーションが表示されるコンポーネントです。

  • pager: ページ番号の配列
  • current: 現在のページ番号
  • category: カテゴリー情報を表すオブジェクト


// components/Pagination.vue

<template>
  <div class="wrapper">
    <ul class="pager">
      <li v-if="current > 1" class="page arrow">
        <nuxt-link :to="getPath(current - 1)">
          <img src="/images/icon_arrow_left.svg" alt="前のページへ" />
        </nuxt-link>
      </li>
      <li v-if="3 < current" class="page">
        <nuxt-link :to="getPath(1)">
          1
        </nuxt-link>
      </li>
      <li v-if="4 < current" class="omission">
        ...
      </li>
      <li
        v-for="p in pager"
        v-show="current - 3 <= p && p <= current + 1"
        :key="p"
        class="page"
        :class="{ active: current === p + 1 }"
      >
        <nuxt-link :to="getPath(p + 1)">
          {{ p + 1 }}
        </nuxt-link>
      </li>
      <li v-if="current + 3 < pager.length" class="omission">
        ...
      </li>
      <li v-if="current + 2 < pager.length" class="page">
        <nuxt-link :to="getPath(pager.length)">
          {{ pager.length }}
        </nuxt-link>
      </li>
      <li v-if="current < pager.length" class="page arrow">
        <nuxt-link :to="getPath(current + 1)">
          <img src="/images/icon_arrow_right.svg" alt="次のページへ" />
        </nuxt-link>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    pager: {
      type: Array,
      required: false,
      default: () => [],
    },
    current: {
      type: Number,
      required: true,
    },
    category: {
      type: Object,
      required: false,
      default: undefined,
    },
  },
  methods: {
    getPath(p) {
      return `/${
        this.category !== undefined ? `category/${this.category.id}/` : ''
      }page/${p}`;
    },
  },
};
</script>


次にこちらのコンポーネントを一覧画面から呼び出します。
その際、pagerはmicroCMSのAPIから返却されるtotalCountと1ページ分の表示件数を元に算出します。
例えば全部で48件のコンテンツがあり、1ページに付き10件表示がしたい場合、[0, 1, 2, 3, 4]という配列が取得できればOKです。

[...Array(Math.ceil(data.totalCount / limit)).keys()]


currentはURLのパスから受け取ることができます。
例えば、/page/2にアクセスがきた場合、params.p2が格納されています。
文字列型で取得されるので、propsに渡す際には数値型にキャストしてあげる必要があります。

実際の一覧画面側のソースコードは次のようになります。

// pages/index.vue

<template>
  <div>
    // 略
    <Pagination
    :pager="pager"
    :current="Number(page)"
    :category="selectedCategory"
    />
  </div>
</template>

<script>
export default {
  async asyncData({ params }) {
    const page = params.p || '1';
    const categoryId = params.categoryId;
    const limit = 10;

    const { data } = await axios.get(
      `https://your-service-id.microcms.io/api/v1/blog?limit=${limit}${
        categoryId === undefined ? '' : `&filters=category[equals]${categoryId}`
      }&offset=${(page - 1) * limit}`,
      { headers: { 'X-MICROCMS-API-KEY': 'your-api-key' } }
    );
    const categories = await axios.get(
      `https://your-service-id.microcms.io/api/v1/categories?limit=100`,
      {
        headers: { 'X-MICROCMS-API-KEY': 'your-api-key' },
      }
    );
    const selectedCategory =
      categoryId !== undefined
        ? categories.data.contents.find((content) => content.id === categoryId)
        : undefined;

    return {
      ...data,
      selectedCategory,
      page,
      pager: [...Array(Math.ceil(data.totalCount / limit)).keys()],
    };
  },
  // 略
};
</script>


また、microCMSブログのソースコードはGitHubに公開されておりますので、そちらも参考にしてみてください。
上記の該当コードはこちら

おわりに

NuxtのJamstack構成におけるページングの実装について解説しました。
一覧ページと個々のページについてはJamstack化できたけど、ページングがよくわからない!という方の助けになれば幸いです。

-----

microCMSは日々改善を進めています。
ご意見・ご要望は管理画面右下のチャット、公式Twitterメールからお気軽にご連絡ください!
引き続きmicroCMSをよろしくお願いいたします!

まずは、無料で試してみましょう。

APIベースの日本製ヘッドレスCMS「microCMS」を使えば、 ものの数分でAPIの作成ができます。

microCMSを無料で始める

microCMSについてお問い合わせ

初期費用無料・14日間の無料トライアル付き。ご不明な点はお気軽にお問い合わせください。

お問い合わせ

microCMS公式アカウント

microCMSは各公式アカウントで最新情報をお届けしています。
フォローよろしくお願いします。

  • X
  • Discord
  • github

ABOUT ME

柴田 和祈
microCMSのデザイン、フロントエンド担当 / ex Yahoo / 2児の父 / 著書「React入門 React・Reduxの導入からサーバサイドレンダリングによるUXの向上まで 」 / Jamstack