作成日:2022年7月2日
NextJs+Typescript+MicroCMS構成のブログ作成チュートリアルは様々なQAサイト、ブログに記事があります。
それら記事を拝見しながら作った本ブログですが、
技術的に一歩踏み込んだところで、「どう実装するんだ?」となってしまった部分がいくつかありました。
その中でも今回は、「ページング付きの、カテゴリ別記事一覧」の実装方法についてまとめてみようという試みです。
今回の記事は、以下の知識があることを前提とした説明になっています。
また、本記事はNextJsでSSGをする方法、MicroCMSのAPI連携部分の実装等、いろいろとすっ飛ばして説明しています。
基本的なブログの作成方法に関する、記事は下記リンクを参考にしてください。
microCMS + Next.jsでJamstackブログを作ってみよう|MicroCMS公式Blogより
環境構築~デプロイの手順がとってもわかりやすくまとまっています。
(MicroCMS公式ブログはわかりやすくて、ためになる優良記事ばかりで、控えめに言って、最高です。
Next.Jsについての記事もたくさんありますので、他記事を拝読することもお勧めします。)
pagesディレクトリに、category/[categoryId]/page/[page].tsxを追加します。
ディレクトリ構成
pages
|-category
|-[categoryId]
|-page
|- [page].tsx <=追加
|-_app.tsx
|-index.tsx
pages.tsx
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
//1ページに表示する最大記事数
const MAX_PAGE =10 as const;
type Props={
data:IMicroCMSBlogRes,
page:number,
categoryName:string,
categoryId:string
}
const CategoryPage:NextPage<Props> =({categoryName,data,page,categoryId})=>{
return(
<></>
)
}
export const getStaticPaths:GetStaticPaths =async () =>{
return //GetStaticPathsの結果オブジェクト
}
export const getStaticProps:GetStaticProps=async({params}:GetStaticPropsContext)=>{
return //Props
}
export default CategoryPage;
GetStaticPaths、GetStaticProps、NextPageの中身はとりあえず空にしています。
個人的な好みで、コンポーネントごとに固有のpropsは type Props={}
で統一していますが、命名は好みで変えてください。
今回使用するtypescriptの型は以下のようになっています。interfaces/microCMS.ts
//MicroCMSAPIからの取得記事
export interface IArticle {
id: string
createdAt: string
updatedAt: string
publishedAt: string
revisedAt: string
title: string
content: string
description:string
eyecatch:{
url: string
height: number
width: number
}
category:ICategory
}
export interface ICategory{
id:string
name:string
}
//MicroCMAPIからのレスポンス固定のパラメータ
export interface IMicroCMSRes{
totalCount: number,
offset: number,
limit: number
}
//BlogエンドポイントのAPIレスポンス
export interface IMicroCMSBlogRes extends IMicroCMSRes{
contents:IArticle[]
}
//CategoryエンドポイントのAPIレスポンス
export interface IMicroCMSCategoriesRes extends IMicroCMSRes{
contents:ICategory[]
}
GetStaticPathsの処理です。
export const getStaticPaths:GetStaticPaths =async () =>{
//全カテゴリ取得
const categories =await GetAllCategories();
const pathsNotFlat = await Promise.all(
categories.map((category:ICategory) => {
//カテゴリごとに記事数を取得し、何ページ必要かを計算
return GetPostsCountInCategories(category.id)
.then((totalCount) => {
const range = (start: number, end: number) =>
[...Array(end - start + 1)].map((_, i) => start + i);
//ページの最大値を計算(記事数0でも表示)
const pagesEnd =Math.max(1,Math.ceil(totalCount / MAX_PAGE));
return range(1, pagesEnd ).map(
page => `/category/${category.id}/page/${page}`
)
});
})
)
const paths=pathsNotFlat.flat();
return { paths, fallback: false }
}
解説していきます。
まず、await GetAllCategories() で、MicroCMSに登録されているブログのカテゴリを全て取得します。
GetAllCategories()内の処理については、MicroCMSに登録されている全カテゴリを取得し、
IMicroCMSCategoriesResの型で返しているだけなので割愛します。
次に、Promise.All()内で、取得したカテゴリごとに、紐づいている記事の数を取得しています。
MicroCMSのデータ取得APIには、SQLで言うJoinにあたるクエリが無い(無いかもしれない)ので、
カテゴリ全取得 → 各カテゴリごとに記事数取得
という、とってもめんどくさいプロセスを踏んでいます。
(今思えば全記事を取得してreduce()でカテゴリごとの記事数をまとめたほうが良かったのかも、、)
GetPostsInCategories()では、該当するIDのカテゴリの記事件数を取得してきます。
関数の中身はこのようになっています。lib/posts.ts
export async function getPostsCountInCategories(id:string):Promise<number>{
const data:IMicroCMSCategoriesRes =await client.get({
endpoint: 'blogs',
queries: {filters: `category[equals]${id}`,fields: "totalcount"}
});
return data.totalCount;
}
APIでカテゴリに紐づいているブログ数を取得しています。
この関数をPromiseで返し、PromiseAll()で全カテゴリの件数取得を待ちます。
続いて、GetPostsCountInCategories()のメソッドチェーンになっている.then()の中身についてです。
range()で、表示するすべてのページ番号を配列で取得します。
const range = (start: number, end: number) =>[...Array(end - start + 1)].map((_, i) => start + i);
start~endの数値の連番を配列を返します。
range(1,5)は[1,2,3,4,5]を返すといった具合です。
ちなみに、
Array(end-start+1).map((_, i) => start + i);//×Array()がemptyの配列を返すため、map()が機能しない。
としてしまうと、一見動きそうですが動かないので注意です。参考:【JavaScript】Array.prototype.map()とemptyな配列
動的に連番を取得する上記の関数は地味によく使うので、テンプレとして覚えておくことをお勧めします。
カテゴリごとの記事の最大ページ数を計算します。
const pagesEnd = Math.max(1,Math.ceil(totalCount / MAX_PAGE));
「総記事数÷1ページに表示したい記事数」を切り上げることで、ページ数が取得できます。
この時の注意点としては、記事数0、すなわちページ数0でもページを表示できるようにすることです。
今回はMath.max()を使用して、ページ数の最小値を1にしています。
SSGするページのルーティングを文字列で返します。
当然ですが生成するパスは、category/[categoryId]/page/[page].tsxのディレクトリ名と一致させてください。
return range(1, pagesEnd ).map(
page => `/category/${item.id}/page/${page}`
)
pathsオブジェクトで返したい場合は、paramsを持ったオブジェクトを返してください。
//これでもいけます。
return range(1, pagesEnd ).map(
page => {
params:{
category:item.id
page:page
}
}
)
await PromiseAll()によって得られる値は、配列にしたルーティングを、カテゴリでごとにさらに配列したものになります。
string[][]となってしまっているので、string[]に直してあげます。
const paths=pathsNotFlat.flat();//string[][]=>string[]
getStaticPropsの処理です。
export const getStaticProps:GetStaticProps=async({params})=>{
const categoryId = String(params?.categoryId);//カテゴリIDを取得
const categoryPage = Number(params?.page);//ページ
const data = await getPostsInCategories(MAX_PAGE,categoryPage-1,categoryId);//記事を取得
const categoryName = await GetCategoryName(categoryId);//カテゴリ名を取得
return {
props:{
data,
categoryPage,
categoryName,
categoryId
}
}
}
paramsのメンバにはgetStaticPathsで定義したcategoryId,pageが含まれていますが、
今回の例では、getStaticPathsの戻り値を厳密に型指定していないので、補完が効かないことに注意してください。
getStaticPaths、getStaticPropsの引数を厳密に型指定したい方はこちらが参考になります。
[Next] getStaticPropsの型の付け方、型定義について|zenn
一覧にする記事情報をawait getPostsInCategories()で取得します。
ブログ取得のクエリにoffsetとlimitを指定して、
「先頭から何件目」を、「何件取得するのか」を指定することで、ページごとの記事一覧を取得できます。
export async function getPostsInCategories(count:number,offset:number,id:string):Promise<IMicroCMSBlogRes>{
const data:IMicroCMSBlogRes =await client.get({ endpoint: 'blogs',
queries: {filters: `category[equals]${id}`,
limit: count,
offset: offset}});
return data;
}
カテゴリ名はわざわざAPIで取得せずとも、記事一覧から取得可能ですが、今回はわかりやすくするために、APIで取得するようにしています。await GetCategoryName()
で、カテゴリIDに対応するカテゴリ名を取得しています。
最後に、getStaticPropsによって取得した記事の一覧データを使って、NextPage関数内でページネーションを実装します。
ページネーションは、MUIを使うと簡単に実装できます。
MUIをインストールします。
// npm
npm install @mui/material @emotion/react
// yarn
yarn add @mui/material @emotion/react
@mui/materialからPaginationコンポーネントをインポートし、各propsを割り当てます。
詳しくは、MUI/Pagination|MUI公式を参考にしてください。
import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
import { Pagination } from '@mui/material';//追加
//1ページに表示する最大記事数
const MAX_PAGE =10 as const;
//~~~~~~~~~~~~~~~
const CategoryPage:NextPage<Props> =({categoryName,data,page,categoryId})=>{
const totalPagesCount = Math.ceil(data.totalCount/MAX_PAGE);//記事数からページ数を計算
return(
<>
<Pagination
count={totalPagesCount}
page={page}
siblingCount={3} />
</>
)
}
ページネーションをnext/linkで遷移できるようにします。
next/routerからuseRouterモジュールをインポートし、
PaginationコンポーネントのonChangeイベントで、routerのpushイベントを発火させると、
任意のページに遷移できます。
const CategoryPage:NextPage<Props> =({categoryName,data,page,categoryId})=>{
const totalPagesCount = Math.ceil(data.totalCount/MAX_PAGE);
const router = useRouter();
const handlePageChange = (event: React.ChangeEvent<unknown>, page: number) => {
let {pathname} = router;//現在のドメイン以降のパスを取得(パラメータは取得できない)
router.query.page=page:
router.push({
pathname:pathname,
query:router.query
})
}
return(
<>
<Pagination
onChange={handlePageChange}//追加
count={totalPagesCount}
page={page}
siblingCount={3} />
</>
)
}
useRouter()から取得できるpathNameは、ドメイン以降のパスを取得できますが、パラメータは取得できません。
たとえば、category/blog/page/1のページで取得できるpathNameは”category/[categoryId]/page/[page]”となります。
よって、ページ遷移をしたいのであれば、router.queryを書き換えるのがよさそうです。
最後に、無理やり一覧画面を作るとすればこんな感じです。
const CategoryPage:NextPage<Props> =({categoryName,data,page,categoryId})=>{
const totalPagesCount = Math.ceil(data.totalCount/MAX_PAGE);
const articles = datas.contents;
const router = useRouter();
const handlePageChange = (event: React.ChangeEvent<unknown>, page: number) => {
let {pathname} = router;//現在のドメイン以降のパスを取得(パラメータは取得できない)
router.query.page=page:
router.push({
pathname:pathname,
query:router.query
})
}
return(
<>
<h1>{categoryName}</h1>
<ul>
{articles.map(article=>
<li>{article.title}</li>
)}
</ul>
<Pagination
onChange={handlePageChange}
count={totalPagesCount}
page={page}
siblingCount={3} />
</>
)
}
後は見た目のみですね!スタイルは適当に当てましょう。(本ブログはChakraUIを使ってスタイリングしています)
今回はNextJs+MicroCMSのブログでの、カテゴリ別記事一覧の実装方法について説明しました。
ただのカテゴリ別一覧なのに、意外と骨が折れる工程でしたね、、
まだまだ改善の余地があると思うので、ご自身で実装する際は自分なりに工夫してみてください!