microCMS+Astroで記事取得からページネーション実装まで

microCMS+Astroを使う機会があったのでメモ。スタイルまわりにはTailwindCSSを導入しています。

SDKを使ってmicroCMSの情報を取得する

microCMSにはSDKがあるので、それを使って楽に情報を取得できます。

microcmsio/microcms-js-sdk: microCMS JavaScript SDK.

SDK|microCMS|APIベースの日本製ヘッドレスCMS

src/library/microcms.ts

import type { MicroCMSQueries } from "microcms-js-sdk";
import { createClient } from "microcms-js-sdk";

// types
import type { itemType } from '@/types/api'

//ドメインとAPIキーを入れる
const client = createClient({
  serviceDomain: import.meta.env.MICROCMS_URL,
  apiKey: import.meta.env.MICROCMS_KEY,
});

/**
 * 個別記事取得
 */
export const getItem = async (
  contentId: string,
  queries?: MicroCMSQueries
) => {
  return await client.getListDetail<itemType>({
    endpoint: "item",
    contentId,
    queries,
  });
};

src/types/api/index.ts

//item
export type itemType = {
  id: string,
  name: string,
  category: {
    id: string,
    name: string,
    slug: string,
  }[],
  contents:string,
}

ドメインについて

//ドメインとAPIキーを入れる
const client = createClient({
  serviceDomain: import.meta.env.MICROCMS_URL,
  apiKey: import.meta.env.MICROCMS_KEY,
});

serviceDomain には、画像の部分を入れます。

全件取得するには

microCMS ヘルプ | 101件以上のコンテンツを取得するにはどうしたらよいですか?

公式のヘルプによると、 getAllContents() で全件取得できる。

/**
 * 全件記事取得
 */
export const getAllItemList = async (
  queries?: MicroCMSQueries
) => {
  return await client.getAllContents<itemType[]>({ endpoint: "item", queries });
}

getAllContentsの戻り値

// 
const response = await getAllItemList({
    fields: ["id", "name", "category"],
  });


//全件分のデータが配列で戻ってくる
[
  {
    id: '123456',//microCMSの記事ID
    name: 'テストデータ',
    category: [
      {
        id: '123456',
        name: 'ダミーカテゴリー',
        slug: 'dummy',
      },
      {
        id: '123456',
        name: 'ダミーカテゴリー2',
        slug: 'dummy2',
      }
    ]
  },
  {
    id: '7890',//microCMSの記事ID
    name: 'テストデータ2',
    category: [
      {
        id: '123456',
        name: 'ダミーカテゴリー',
        slug: 'dummy',
      }
    ]
  },

  ...

  ]

記事一覧の実装

src/pages/item/[page].astro

---
import Layout from "@/layouts/Layout.astro";

import { getAllItemList } from "@/library/microcms";
import type { itemListSingleType } from "@/types/api";
import ArchiveListSingle from "@/components/atoms/ArchiveListSingle.astro";
import PageNavi from "@/components/atoms/navi/PageNavi.astro";

const PER_PAGE = import.meta.env.ITEM_PER_PAGE; //1ページにつき、いくつ記事を入れるか

export async function getStaticPaths({ paginate }: any) {
  const response = await getAllItemList({
    fields: ["id", "name", "category"],
  });

  const list = response;
  
  //paginate()関数で返す
  return paginate(list, { pageSize: Number(PER_PAGE) });
}

//受け取る型
interface Props {
  page: {
    data: itemListSingleType[];
    start: number;
    end: number;
    size: number;
    total: number;
    currentPage: number;
    lastPage: number;
    url: {
      current: string;
      next: string;
      prev: string;
    };
  };
}
const { page } = Astro.props;

---

<Layout title="一覧ページ" description={`いま${page.currentPage}ページ目`}>
  <main>
      <ul class="grid gap-4 grid-cols-3">
        {page.data.map((content) => <ArchiveListSingle {...content} />)}
      </ul>
      {page && <PageNavi url="/item" itemList={page} />}
  </main>
</Layout>

src/types/api/index.ts

//itemListSingle
export type itemListSingleType = {
  id: string,
  name: string,
  category: {
    id: string,
    name: string,
    slug: string,
  }[],
}

src/components/atoms/ArchiveListSingle.astro (記事リスト表示用のコンポーネント)

---
const { id, name, category } = Astro.props;
---

<li
  class="list-none rounded-xl border-solid bg-[#fbfbfb] shadow-md text-[#333]"
>
  <a href={`/item/${id}`} class="p-4 block">
    <p class="text-lg font-bold mb-2">{name}</p>
    <CategoryList
      list={category}
      className="flex flex-wrap gap-2"
    />
  </a>
</li>

src/components/atoms/list/CategoryList.astro (記事リストの中のカテゴリー表示用のコンポーネント)

---
interface Props {
  list:{
    id: string;
    name: string;
    slug: string;
  }[],
  className:string,
}

  const {
    list,
    className,
    link,
  } = Astro.props;
  ---

{
  0 < list.length && (
      <div class={className}>
      {
        list.map((itemCategory) => {
          return (
              <span class={`bg-primary text-white px-2 py-1 text-[12px] rounded-lg ${itemCategory.slug}`} >
                {itemCategory.name}
              </span>
          )

        })
      }
    </div>
    )
}

2ページ目以降のルーティングを作成

Astroにはpaginate() 関数があり、それをを使用して2ページ目以降のページを生成します。

src/pages/item/[page].astro

---
import { getAllItemList } from "@/library/microcms";
import type { itemListSingleType } from "@/types/api";

const PER_PAGE = import.meta.env.ITEM_PER_PAGE; //1ページにつき、いくつ記事を入れるか

export async function getStaticPaths({ paginate }: any) {
  const response = await getAllItemList({
    fields: ["id", "name", "category"],
  });

  const list = response;
  
  //paginate()関数を使う
  return paginate(list, { pageSize: Number(PER_PAGE) });
}

//受け取る型
interface Props {
  page: {
    data: itemListSingleType[];
    start: number;
    end: number;
    size: number;
    total: number;
    currentPage: number;
    lastPage: number;
    url: {
      current: string;
      next: string;
      prev: string;
    };
  };
}
const { page } = Astro.props;

---
  <main>
    <p>{`全部で${page.lastPage}ページ`}</p>
    <p>{`いま${page.currentPage}ページ目です`}</p>
      {page.data.map((content) => {
        return (
          <>
            <p>記事名:{content.name}</p>
          </>
        )
      })}
  </main>

参考サイト

astro DOCS | ルーティング | Docs

ページネーションの実装

src/components/atoms/navi/PageNavi.astro

---
import type { itemListSingleType } from "@/types/api";
interface Props {
  url: string;
  itemList: {
    data: itemListSingleType[];
    start: number;
    end: number;
    size: number;
    total: number;
    currentPage: number;
    lastPage: number;
    url: {
      current: string;
      next: string;
      prev: string;
    };
  };
}
const { url, itemList } = Astro.props;
const STEP = 2; //現在のページの前後表示数

const currentPage = Number(itemList.currentPage); //現在のページ数

let maxPage = itemList.lastPage; //全ページ数

let firstFlg = false; //・・・と最初のリンクを表示するかどうか
let lastFlg = false; //・・・と最後のリンクを表示するかどうか

let firstStep = currentPage - STEP; // 現在のページの前に表示する最初のページ
let lastStep = currentPage + STEP; //現在のページの後に表示する最後のページ

if (firstStep <= 0) {
  firstStep = 1;
  firstFlg = false;
} else {
  if (1 < firstStep) {
    firstFlg = true;
  } else {
    firstFlg = false;
  }
}

if (maxPage <= lastStep) {
  lastStep = maxPage;
  lastFlg = false;
} else {
  lastFlg = true;
}

/**
 * 配列を出力
 * @param start 
 * @param end 
 */
const range = (start: number, end: number) =>
  [...Array(end - start + 1)].map((_, i) => start + i);

---

<>
  <div class="flex justify-center">
    <ul class="flex gap-4 mt-1 c-pageNavi text-[#333]">
      {
        1 < currentPage && (
          <li class="flex bg-white rounded-md shadow-md ">
            <a
              href={`${url}/page/${currentPage - 1}`}
              class="flex items-center px-3 py-2"
            >
              <
            </a>
          </li>
        )
      }
      {
        firstFlg && (
          <>
            <li class="flex bg-white rounded-md shadow-md">
              <a href={`${url}/page/1`} class="flex items-center px-3 py-2">
                1
              </a>
            </li>
            <li class="flex text-white items-center">...</li>
          </>
        )
      }
      {
        range(firstStep, lastStep).map((number) => (
          <li
            class={`flex bg-white rounded-md shadow-md ${number === currentPage ? "is-current" : ""}`}
          >
            {number === currentPage ? (
              <span class="flex items-center px-3 py-2">{number}</span>
            ) : (
              <a
                href={`${url}/page/${number}`}
                class="flex items-center px-3 py-2"
              >
                {number}
              </a>
            )}
          </li>
        ))
      }
      {
        lastFlg && (
          <>
            <li class="flex text-white items-center">...</li>
            <li class="flex bg-white rounded-md shadow-md">
              <a
                href={`${url}/page/${maxPage}`}
                class="flex items-center px-3 py-2"
              >
                {maxPage}
              </a>
            </li>
          </>
        )
      }
      {
        currentPage  < maxPage && (
          <li class="flex bg-white rounded-md shadow-md ">
            <a
              href={`${url}/page/${currentPage + 1}`}
              class="flex items-center px-3 py-2"
            >
              >
            </a>
          </li>
        )
      }
    </ul>
  </div>
</>

src/pages/item/[page].astro (PageNaviコンポーネントを読み込む)

---
import Layout from "@/layouts/Layout.astro";

import { getAllItemList } from "@/library/microcms";
import type { itemListSingleType } from "@/types/api";
import ArchiveListSingle from "@/components/atoms/ArchiveListSingle.astro";
import PageNavi from "@/components/atoms/navi/PageNavi.astro";

const PER_PAGE = import.meta.env.ITEM_PER_PAGE; //1ページにつき、いくつ記事を入れるか

export async function getStaticPaths({ paginate }: any) {
  const response = await getAllItemList({
    fields: ["id", "name", "category"],
  });

  const list = response;
  
  //paginate()関数を使う
  return paginate(list, { pageSize: Number(PER_PAGE) });
}

//受け取る型
interface Props {
  page: {
    data: itemListSingleType[];
    start: number;
    end: number;
    size: number;
    total: number;
    currentPage: number;
    lastPage: number;
    url: {
      current: string;
      next: string;
      prev: string;
    };
  };
}
const { page } = Astro.props;

---

<Layout title="一覧ページ" description={`いま${page.currentPage}ページ目`}>
  <main>
      <ul class="grid gap-4 grid-cols-3">
        {page.data.map((content) => <ArchiveListSingle {...content} />)}
      </ul>

      //propsの情報をコンポーネントへ渡す
      {page && <PageNavi url="/item" itemList={page} />}
  </main>
</Layout>

こんな感じのものができる

参考サイト

microcmsio/microcms-js-sdk: microCMS JavaScript SDK.

SDK|microCMS|APIベースの日本製ヘッドレスCMS

astro DOCS | ルーティング | Docs

  • コーダーのための計算機