Next.js, microCMS, Chakra-UIで作るブログシリーズの4つ目の記事です。
今回はタグでの絞り込み機能を実装させていきます。
親からタグオブジェクトを受け取って、リンクラベルを表示するイメージです。
/* src/components/TagLink.tsx */
import {
Link,
Tag,
TagLabel
} from "@chakra-ui/react";
import NextLink from "next/link";
import { FC } from "react";
import type { PostTag } from "types/blog";
export const TagLink: FC<{ tag: PostTag }> = ({ tag }) => {
return (
<NextLink key={tag.id} href={`/tags/${tag.id}/page/1`} passHref>
<Link>
<Tag variant='subtle' colorScheme='cyan'>
<TagLabel fontSize="md">{tag.name}</TagLabel>
</Tag>
</Link>
</NextLink>
);
};
つぎにPostList.tsx
を編集して、こちらのコンポーネントを呼び出します。
/* src/components/PostList.tsx */
import type { Post } from 'types/blog';
import {
Box,
Heading,
Stack,
Link,
Text,
Button,
// 追加
Wrap,
WrapItem,
} from "@chakra-ui/react";
import { DateTime } from 'components/DateTime';
// 追加
import { TagLink } from 'components/TagLink';
type Props = {
posts: Post[]
}
export const PostList = ({ posts }: Props) => {
return (
<>
{posts.map(post => (
<Box key={post.id}>
{/* 追加 */}
<Wrap>
{post.tag.map(tag => (
<WrapItem key={tag.id}>
<TagLink tag={tag} />
</WrapItem>
))}
</Wrap>
...省略
</Box>
))}
</>
)
}
無事に表示されたことを確認できたら、タグで絞り込まれた一覧を表示するページを作っていきましょう。
TagLink.tsx
内ではリンクURLを/tags/${tag.id}/page/1
としていました。
なのでpages内にtags→[tagId]→page→[id].tsxという階層でファイルを作成をします。
params.tagId
でタグのIDが、params.id
でページナンバーが受け取れるような構成ですね。
少し長いです。
/* src/pages/tags/[tagId]/page/[id].tsx */
import { Pagination } from 'components/Pagination';
import { client } from 'libs/client';
import { GetStaticPaths, GetStaticProps, } from "next";
import type { Post } from "types/blog";
import { Header } from 'components/Header'
import { PostList } from 'components/PostList';
import type { PostTag } from 'types/blog';
import {
Box,
Container,
Heading
} from "@chakra-ui/react";
import { BLOG_PER_PAGE } from 'settings/siteSettings';
type Props = {
posts: Post[]
totalCount: number
currentPage: number
};
export default function BlogTagId({ posts, totalCount, currentPage }: Props) {
return (
<Box>
<Header />
<Container as="main" maxW="container.lg" marginTop="4" marginBottom="16">
<Heading as="h1" marginBottom="8" fontSize="2xl">
Home
</Heading>
<PostList posts={posts} />
<Pagination totalCount={totalCount} currentPage={currentPage} />
</Container>
</Box>
);
}
const getAllTagPagePaths = async () => {
const resTag = await client.getList<PostTag>({
endpoint: 'tag',
// タグを全て取得する必要があるが、記事数に比べると限定的
// これ以上は増えないだろう、という値をいれておく
queries: { limit: 100 }
})
const paths: string[][] = await Promise.all(
// タグごとに繰りかえして、紐づいた記事一覧のGetリクエストを行い、totalCountを取得していく
resTag.contents.map((item: PostTag) => {
const result = client
.getList<Post>({
endpoint: 'post',
queries: {
filters: `tag[contains]${item.id}`,
},
})
// タグごとにページ1…ページ2…とパスを生成していく
.then(({ totalCount }) => {
const range = (start: number, end: number) =>
[...Array(end - start + 1)].map((_, i) => start + i)
return range(1, Math.ceil(totalCount / BLOG_PER_PAGE)).map(
(repo) => `/tags/${item.id}/page/${repo}`
)
})
return result
})
)
// タグごとに配列になっているので、最後にフラットにして返す
return paths.flat()
}
export const getStaticPaths: GetStaticPaths = async () => {
const paths = await getAllTagPagePaths()
return { paths, fallback: false }
}
export const getStaticProps: GetStaticProps<Props, { tagId: string, id: string }> = async ({ params }) => {
if (!params) throw new Error("Error Tag ID Not Found");
const tagId = params.tagId
const pageId = Number(params.id);
const data = await client.getList<Post>({
endpoint: "post",
queries: {
offset: (pageId - 1) * BLOG_PER_PAGE,
limit: BLOG_PER_PAGE, filters: `tag[contains]${tagId}`
}
});
return {
props: {
posts: data.contents,
totalCount: data.totalCount,
currentPage: pageId,
},
};
};
少し複雑なのは、getAllTagPagePaths
関数でしょうか。
今回のブログは静的生成する関係で有りうるパスをNext側へ全て渡してあげる必要があります。
ページネーションを作った時と基本的に同じ考えですが、それをタグごとに繰り返さないといけません。
以下のような処理の流れです。
タグを全て取得のところですが、今回はlimitを100にして設定しました。
const resTag = await client.getList<PostTag>({
endpoint: 'tag',
// タグを全て取得する必要があるが、記事数に比べると限定的
// これ以上は増えないだろう、という値をいれておく
queries: { limit: 100 }
})
ここはtotalCountを取得して、totalCountをlimitに指定する方が確実性は高まると思います。
とはいえ、そのために一回リクエストを投げなくてもいいような場面である気もするので、こうしています。
この時点でタグをクリックすると絞り込まれたページが表示されます。
生活
をクリックした状態。
プログラミング
をクリックした状態
タグごとに絞り込まれた記事が表示されました。
現状、1ページ目では絞り込まれた状態です。
しかし、2ページ目にアクセスすると、通常のページ送りの記事一覧に遷移してしまいます。
/tags/programing/page/1
みたいなリンクから/page/2
に遷移してしまうということですね。
こちらを解消するために、Pagination.tsxを編集しましょう。
/* src/components/Pagination.tsx */
import {
Box,
HStack,
Link,
Text
} from "@chakra-ui/react";
import { BLOG_PER_PAGE } from 'settings/siteSettings'
type Props = {
totalCount: number;
currentPage?: number;
// 追加 オプションでタグIDを受け取れるように
tagId?: string;
};
// 変更 tagIdを受け取る
export const Pagination = ({ totalCount, currentPage = 1, tagId }: Props) => {
const range = (start: number, end: number) =>
[...Array(end - start + 1)].map((_, i) => start + i)
const pageCount = Math.ceil(totalCount / BLOG_PER_PAGE)
// 追加 tagIdが存在するときはタグ一覧用のページリンクを返す
const getPath = (p: number) => {
if (tagId) return `/tags/${tagId}/page/${p}`
return `/page/${p}`
}
const getPaginationItem = (p: number) => {
if (p === currentPage) return <Text color="gray.700" fontSize="3xl">{p}</Text>
// 変更 getPath関数でリンクを取得
return <Link href={getPath(p)} color="gray.400" fontSize="3xl">{p}</Link>
}
return (
<HStack spacing='10' justifyContent="center">
{range(1, pageCount).map((number, index) => (
<Box key={index}>
{getPaginationItem(number)}
</Box>
))}
</HStack >
);
};
タグIDをオプショナルに受け取り、存在する場合はタグ用の一覧を返すように編集しました。
次にタグ一覧ページから呼び出します。
/* src/pages/tags/[tagId]/page/[id].tsx */
import { Pagination } from 'components/Pagination';
... 省略
type Props = {
posts: Post[]
totalCount: number
currentPage: number
// 追加
tag: PostTag
};
// 変更 tagを受け取る
export default function BlogTagId({ posts, totalCount, currentPage, tag }: Props) {
return (
<Box>
<Header />
<Container as="main" maxW="container.lg" marginTop="4" marginBottom="16">
<Heading as="h1" marginBottom="8" fontSize="2xl">
Home
</Heading>
<PostList posts={posts} />
{/* 変更 tagのidをpaginationに渡す */}
<Pagination totalCount={totalCount} currentPage={currentPage} tagId={tag.id} />
</Container>
</Box>
);
}
...省略
export const getStaticProps: GetStaticProps<Props, { tagId: string, id: string }> = async ({ params }) => {
if (!params) throw new Error("Error Tag ID Not Found");
const tagId = params.tagId
const pageId = Number(params.id);
const data = await client.getList<Post>({
...省略
});
// 追加 タグオブジェクトを取得
const tag = await client.getListDetail<PostTag>({
endpoint: 'tag', contentId: tagId
})
return {
props: {
posts: data.contents,
totalCount: data.totalCount,
currentPage: pageId,
// 追加
tag: tag
},
};
};
これで正しくページ送りがされるようになりました。
最後にタグで絞り込まれた時にパンくずリストを使って、ユーザーに表示をするようにしていきましょう。
コンポーネント内にBreadcrumbs.tsxを作成します。
/* src/components/Breadcrumbs.tsx */
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
} from "@chakra-ui/react";
import { ChevronRightIcon } from "@chakra-ui/icons";
import type { PostTag } from 'types/blog';
type Props = {
tag?: PostTag;
};
export const Breadcrumbs = ({ tag }: Props) => {
return (
<Breadcrumb spacing='8px' separator={<ChevronRightIcon color='gray.500' fontSize="xl" fontWeight="bold" />} mb="8">
<BreadcrumbItem>
<BreadcrumbLink href='/' fontSize="2xl" fontWeight="bold" >Home</BreadcrumbLink>
</BreadcrumbItem>
{tag && (
<BreadcrumbItem>
<BreadcrumbLink href={`/tags/${tag.id}/page/1`} fontSize="2xl" fontWeight="bold">{tag.name}</BreadcrumbLink>
</BreadcrumbItem>
)}
</Breadcrumb>
);
};
任意でタグが渡された場合はタグ名を表示をするようなイメージですね。
あとはこれを各一覧コンポーネントから呼び出します。
/* src/pages/index.tsx */
// 追加
import { Breadcrumbs } from 'components/Breadcrumbs';
const Home: NextPage<Props> = ({ posts, totalCount }) => {
return (
<>
<Header />
<Container as="main" maxW="container.lg" marginTop="4" marginBottom="16">
{/* 変更 */}
<Breadcrumbs />
<PostList posts={posts} />
<Pagination totalCount={totalCount}></Pagination>
</Container>
</>
)
}
/* src/pages/page/[id].tsx */
import { Breadcrumbs } from 'components/Breadcrumbs';
export default function BlogPageId({ posts, totalCount, currentPage }: Props) {
return (
<Box>
<Header />
<Container as="main" maxW="container.lg" marginTop="4" marginBottom="16">
<Breadcrumbs />
<PostList posts={posts} />
<Pagination totalCount={totalCount} currentPage={currentPage} />
</Container>
</Box>
);
}
/* src/pages/tags/[tagId]/page/[id].tsx */
import { Breadcrumbs } from 'components/Breadcrumbs';
export default function BlogTagId({ posts, totalCount, currentPage, tag }: Props) {
return (
<Box>
<Header />
<Container as="main" maxW="container.lg" marginTop="4" marginBottom="16">
<Breadcrumbs tag={tag} />
<PostList posts={posts} />
<Pagination totalCount={totalCount} currentPage={currentPage} tagId={tag.id} />
</Container>
</Box>
);
}
タグ付き一覧の時のみ、タグ名が良い感じに表示されました。
大体やりたいことができた気がするので、一旦このシリーズを終わりにしようと思います。
その他Next.js関連の記事を書く際は個別に切り出していきます。
ここまでのソースコードはgithubにアップしました。
https://github.com/qlitre/nextjs-microcms-chakra-blog-template