Next.js × microCMSで作ったブログにカテゴリページを実装

# はじめに
ブログサイトを作る過程で学んだことを、備忘録目的で投稿しています。
自身は駆け出しエンジニアであり、React自体がほぼ初学者のため、誤った認識・理解をしている可能性があります。
万が一参考にする場合は、上記の点を考慮した上でご一読ください。
また、スタイリングについては割愛しています。

この記事は、過去に投稿した[Next.js × microCMSでブログサイト作成した際に学んだことを書き留める](https://qiita.com/miyaayim_eng/items/62f4267280a9f3cfaa4d)のプロジェクトを元に説明しています。

# 目的
記事一覧に、カテゴリで絞り込みをしたカテゴリページを作成する。

# 作業環境
Windows10 Pro x64
Node.js: 20.14.0
npm: 10.3.0
Next.js: 14.2.3（App Router使用）

# 手順

## カテゴリ一覧を取得
microCMSで登録したカテゴリを全て取得します、作りは記事一覧と同じなので説明は割愛します。
```src/libs/microcms.js
// カテゴリー一覧を取得
export const getCategoriesList = async (queries) => {
  try {
    const response = await client.getList({
      endpoint: "categories",
      queries,
    });
    return response;
  } catch (error) {
    console.error("getCategoriesListでエラーが発生しました", error);
    notFound();
  }
};
```

取得したカテゴリを表示します。
カテゴリページへのリンク先は`href={`/category/${category.id}`}`とします。
それ以外は記事一覧のときとほぼ同じなので説明は割愛します。
```/src/app/page.jsx
import { notFound } from "next/navigation";
import styles from "./page.module.scss";
import { LIMIT } from "@/constants";
import { Cards } from "@/components/Cards";
import { Categories } from "@/components/Categories";
import { getArticlesList, getCategoriesList } from "@/libs/microcms";

export default async function Home() {
  // ブログ一覧を取得
  const articlesListQueries = { limit: LIMIT };
  const articlesListResponse = await getArticlesList(articlesListQueries).catch(() => notFound());
  const { contents: articles } = articlesListResponse;

  // カテゴリ一覧を取得
  const categoriesListQueries = { limit: 100 };
  const categoriesListResponse = await getCategoriesList(categoriesListQueries).catch(() => notFound());
  const { contents: categories } = categoriesListResponse;

  return (
    <main className={styles.main}>
      <h1 className={styles.title}>全ての記事</h1>
      <div className={styles.container}>
        <div>
          <p>記事一覧</p>
          <ul className={styles.cards}>
            <Cards articles={articles} />
          </ul>
        </div>
        <div>
          <p>カテゴリ一覧</p>
          <ul className={styles.cards}>
            <Categories categories={categories} />
          </ul>
        </div>
      </div>
    </main>
  );
}

```


```src/components/Categories/index.jsx
import styles from "./index.module.scss";
import { Category } from "@/components/Category";

export const Categories = async ({ categories }) => {
  return (
    <ul className={styles.categories}>
      {categories.map((category) => (
        <Category category={category} key={category.id} />
      ))}
    </ul>
  );
};

```

```src/components/Category/index.jsx
import Link from "next/link";
import styles from "./index.module.scss";

export const Category = async ({ category }) => {
  return (
    <li className={styles.category}>
      <Link href={`/category/${category.id}`} className={styles.link}>
        {category.name}
      </Link>
    </li>
  );
};

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3310435/5e2f81ef-2943-3fb6-2906-529a493e271c.png)



## カテゴリページの作成
カテゴリページを作成します。
記事一覧ページのファイルをベースとするため、記事一覧のファイルをコピーしたのち、
`app`フォルダ配下にコピーした`page.jsx`を`/app/category/[categoryId]/page.jsx`として作成します。

`page.jsx`に追記していきます。
まず、`{ params }`でURLのカテゴリIDを取得し、それをクエリパラメータの`filters`を使用して絞り込みします。
これで、APIでデータ取得したさいに必要なデータのみを取得することができます。
細かい意味は、microCMSのリファレンスを参考にすると良いとおもいます。

```/app/category/[categoryId]/page.jsx
import { notFound } from "next/navigation";
import styles from "./page.module.scss";
import { LIMIT } from "@/constants";
import { getArticlesList, getCategoriesList } from "@/libs/microcms";
import { Cards } from "@/components/Cards";
import { Categories } from "@/components/Categories";

export default async function Page({ params }) {
  // URLからカテゴリIDを取得
  const currentCategory = params.categoryId;
  // ブログ一覧を取得
  const filters = `category[equals]${currentCategory}`;
  const articlesListQueries = { limit: LIMIT, filters: filters };
  const articlesListResponse = await getArticlesList(articlesListQueries).catch(() => notFound());
  const { contents: articles } = articlesListResponse;

  // カテゴリ一覧を取得
  const categoriesListQueries = { limit: 100 };
  const categoriesListResponse = await getCategoriesList(categoriesListQueries).catch(() => notFound());
  const { contents: categories } = categoriesListResponse;

  return (
    <main className={styles.main}>
      <h1 className={styles.title}>「{currentCategory}」の記事</h1>
      <div className={styles.container}>
        <div>
          <p>記事一覧</p>
          <ul className={styles.cards}>
            <Cards articles={articles} />
          </ul>
        </div>
        <div>
          <p>カテゴリ一覧</p>
          <ul className={styles.cards}>
            <Categories categories={categories} />
          </ul>
        </div>
      </div>
    </main>
  );
}

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3310435/80b94feb-e072-fe93-5f22-70f2aa3d053e.png)

## SSGに対応する
SSG（Static Site Generation）の静的なファイルとして生成するようにします。

現時点で`npm run build`を実行すると、下記のようになっており、カテゴリページは動的レンダリングになっているのがわかります。
```console
Route (app)                              Size     First Load JS
┌ ○ /                                    427 B          99.3 kB
├ ○ /_not-found                          871 B          87.8 kB
├ ● /articles/[slug]                     285 B          92.4 kB
├   ├ /articles/3bqtpjwckpa6
├   ├ /articles/2la1jxzdhmy
├   └ /articles/yy3_bxzq2f-0
└ ƒ /category/[categoryId]               390 B          99.2 kB
+ First Load JS shared by all            87 kB
  ├ chunks/23-0627c91053ca9399.js        31.5 kB
  ├ chunks/fd9d1056-2821b0f0cabcd8bd.js  53.6 kB
  └ other shared chunks (total)          1.89 kB


○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
ƒ  (Dynamic)  server-rendered on demand
```

SSGに対応するために、`generateStaticParams`を追記します。
考え方は記事詳細ページでの`generateStaticParams`と同じですので説明は割愛します。
```/src/app/category/[categoryId]/page.jsx
import { notFound } from "next/navigation";
import styles from "./page.module.scss";
import { LIMIT } from "@/constants";
import { getArticlesList, getCategoriesList } from "@/libs/microcms";
import { Cards } from "@/components/Cards";
import { Categories } from "@/components/Categories";

// カテゴリーページの静的パスを作成
export async function generateStaticParams() {
  // カテゴリ一覧を取得
  const queries = { limit: 100, fields: "id" };
  const categoriesListResponse = await getCategoriesList(queries);
  const { contents: categories } = categoriesListResponse;

  const paths = categories.map((category) => {
    return {
      categoryId: category.id,
    };
  });

  // 作成したパスの配列を返します。
  return [...paths];
}

export default async function Page({ params }) {
  // URLからカテゴリIDを取得
  const currentCategory = params.categoryId;
  // ブログ一覧を取得
  const filters = `category[equals]${currentCategory}`;
  const articlesListQueries = { limit: LIMIT, filters: filters };
  const articlesListResponse = await getArticlesList(articlesListQueries).catch(
    () => notFound()
  );
  const { contents: articles } = articlesListResponse;

  // カテゴリ一覧を取得
  const categoriesListQueries = { limit: 100 };
  const categoriesListResponse = await getCategoriesList(
    categoriesListQueries
  ).catch(() => notFound());
  const { contents: categories } = categoriesListResponse;

  return (
    <main className={styles.main}>
      <h1 className={styles.title}>「{currentCategory}」の記事</h1>
      <div className={styles.container}>
        <div>
          <p>記事一覧</p>
          <ul className={styles.cards}>
            <Cards articles={articles} />
          </ul>
        </div>
        <div>
          <p>カテゴリ一覧</p>
          <ul className={styles.cards}>
            <Categories categories={categories} />
          </ul>
        </div>
      </div>
    </main>
  );
}

```

この状態で`nom run build`を実行してみると、カテゴリーページが静的生成に変わったのがわかります。
```console
Route (app)                              Size     First Load JS
┌ ○ /                                    427 B          99.3 kB
├ ○ /_not-found                          871 B          87.8 kB
├ ● /articles/[slug]                     285 B          92.4 kB
├   ├ /articles/3bqtpjwckpa6
├   ├ /articles/2la1jxzdhmy
├   └ /articles/yy3_bxzq2f-0
└ ● /category/[categoryId]               390 B          99.2 kB
    ├ /category/html
    ├ /category/css
    └ /category/javascript
+ First Load JS shared by all            87 kB
  ├ chunks/23-0627c91053ca9399.js        31.5 kB
  ├ chunks/fd9d1056-2821b0f0cabcd8bd.js  53.6 kB
  └ other shared chunks (total)          1.89 kB


○  (Static)  prerendered as static content
●  (SSG)     prerendered as static HTML (uses getStaticProps)
```

# 参考
https://document.microcms.io/content-api/get-list-contents#hdebbdc8e86