mirror of
https://github.com/ChenQihan666/MyBlog-Next.git
synced 2026-08-14 11:29:33 +08:00
feat: add pagination in tag posts (#201)
* feat: add pagination in tag posts Implement pagination in tag posts. Refactor and extract pagination logic and pagination component. Closes: #152 * fix: update breadcrumbs for updated tag pagination
This commit is contained in:
@@ -10,6 +10,18 @@ const breadcrumbList = currentUrlPath.split("/").slice(1);
|
|||||||
// replace Posts with Posts (page number)
|
// replace Posts with Posts (page number)
|
||||||
breadcrumbList[0] === "posts" &&
|
breadcrumbList[0] === "posts" &&
|
||||||
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
|
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
|
||||||
|
|
||||||
|
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
|
||||||
|
// replace [tag] > [page] with [tag] (page number)
|
||||||
|
breadcrumbList[0] === "tags" &&
|
||||||
|
!isNaN(Number(breadcrumbList[2])) &&
|
||||||
|
breadcrumbList.splice(
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
`${breadcrumbList[1]} ${
|
||||||
|
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
|
||||||
|
}`
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav class="breadcrumb" aria-label="breadcrumb">
|
<nav class="breadcrumb" aria-label="breadcrumb">
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
import LinkButton from "./LinkButton.astro";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
prevUrl: string;
|
||||||
|
nextUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentPage, totalPages, prevUrl, nextUrl } = Astro.props;
|
||||||
|
|
||||||
|
const prev = currentPage > 1 ? "" : "disabled";
|
||||||
|
const next = currentPage < totalPages ? "" : "disabled";
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
totalPages > 1 && (
|
||||||
|
<nav class="pagination-wrapper" aria-label="Pagination">
|
||||||
|
<LinkButton
|
||||||
|
disabled={prev === "disabled"}
|
||||||
|
href={prevUrl}
|
||||||
|
className={`mr-4 select-none ${prev}`}
|
||||||
|
ariaLabel="Previous"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class={`${prev}-svg`}>
|
||||||
|
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
|
||||||
|
</svg>
|
||||||
|
Prev
|
||||||
|
</LinkButton>
|
||||||
|
{currentPage} / {totalPages}
|
||||||
|
<LinkButton
|
||||||
|
disabled={next === "disabled"}
|
||||||
|
href={nextUrl}
|
||||||
|
className={`ml-4 select-none ${next}`}
|
||||||
|
ariaLabel="Next"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class={`${next}-svg`}>
|
||||||
|
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
|
||||||
|
</svg>
|
||||||
|
</LinkButton>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination-wrapper {
|
||||||
|
@apply mb-8 mt-auto flex justify-center;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
|
||||||
|
}
|
||||||
|
.disabled-svg {
|
||||||
|
@apply group-hover:!fill-skin-base;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+14
-50
@@ -1,23 +1,20 @@
|
|||||||
---
|
---
|
||||||
import { SITE } from "@config";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import Layout from "@layouts/Layout.astro";
|
import Layout from "@layouts/Layout.astro";
|
||||||
import Main from "@layouts/Main.astro";
|
import Main from "@layouts/Main.astro";
|
||||||
import Header from "@components/Header.astro";
|
import Header from "@components/Header.astro";
|
||||||
import Footer from "@components/Footer.astro";
|
import Footer from "@components/Footer.astro";
|
||||||
|
import Pagination from "@components/Pagination.astro";
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import LinkButton from "@components/LinkButton.astro";
|
import { SITE } from "@config";
|
||||||
import type { CollectionEntry } from "astro:content";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
pageNum: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
posts: CollectionEntry<"blog">[];
|
paginatedPosts: CollectionEntry<"blog">[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pageNum, totalPages, posts } = Astro.props;
|
const { currentPage, totalPages, paginatedPosts } = Astro.props;
|
||||||
|
|
||||||
const prev = pageNum > 1 ? "" : "disabled";
|
|
||||||
const next = pageNum < totalPages ? "" : "disabled";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`Posts | ${SITE.title}`}>
|
<Layout title={`Posts | ${SITE.title}`}>
|
||||||
@@ -25,52 +22,19 @@ const next = pageNum < totalPages ? "" : "disabled";
|
|||||||
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
|
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
posts.map(({ data, slug }) => (
|
paginatedPosts.map(({ data, slug }) => (
|
||||||
<Card href={`/posts/${slug}`} frontmatter={data} />
|
<Card href={`/posts/${slug}`} frontmatter={data} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</Main>
|
</Main>
|
||||||
|
|
||||||
{
|
<Pagination
|
||||||
totalPages > 1 && (
|
{currentPage}
|
||||||
<nav class="pagination-wrapper" aria-label="Pagination">
|
{totalPages}
|
||||||
<LinkButton
|
prevUrl={`/posts${currentPage - 1 !== 1 ? "/" + (currentPage - 1) : ""}`}
|
||||||
disabled={prev === "disabled"}
|
nextUrl={`/posts/${currentPage + 1}`}
|
||||||
href={`/posts${pageNum - 1 !== 1 ? "/" + (pageNum - 1) : ""}`}
|
/>
|
||||||
className={`mr-4 select-none ${prev}`}
|
|
||||||
ariaLabel="Previous"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class={`${prev}-svg`}>
|
|
||||||
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
|
|
||||||
</svg>
|
|
||||||
Prev
|
|
||||||
</LinkButton>
|
|
||||||
<LinkButton
|
|
||||||
disabled={next === "disabled"}
|
|
||||||
href={`/posts/${pageNum + 1}`}
|
|
||||||
className={`ml-4 select-none ${next}`}
|
|
||||||
ariaLabel="Next"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class={`${next}-svg`}>
|
|
||||||
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
|
|
||||||
</svg>
|
|
||||||
</LinkButton>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Footer noMarginTop={totalPages > 1} />
|
<Footer noMarginTop={totalPages > 1} />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
|
||||||
.pagination-wrapper {
|
|
||||||
@apply mb-8 mt-auto flex justify-center;
|
|
||||||
}
|
|
||||||
.disabled {
|
|
||||||
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
|
|
||||||
}
|
|
||||||
.disabled-svg {
|
|
||||||
@apply group-hover:!fill-skin-base;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry } from "astro:content";
|
||||||
|
import Layout from "@layouts/Layout.astro";
|
||||||
|
import Main from "@layouts/Main.astro";
|
||||||
|
import Header from "@components/Header.astro";
|
||||||
|
import Footer from "@components/Footer.astro";
|
||||||
|
import Card from "@components/Card";
|
||||||
|
import Pagination from "@components/Pagination.astro";
|
||||||
|
import { SITE } from "@config";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
paginatedPosts: CollectionEntry<"blog">[];
|
||||||
|
tag: string;
|
||||||
|
tagName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentPage, totalPages, paginatedPosts, tag, tagName } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
|
||||||
|
<Header activeNav="tags" />
|
||||||
|
<Main
|
||||||
|
pageTitle={[`Tag:`, `${tagName}`]}
|
||||||
|
titleTransition={tag}
|
||||||
|
pageDesc={`All the articles with the tag "${tagName}".`}
|
||||||
|
>
|
||||||
|
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
paginatedPosts.map(({ data, slug }) => (
|
||||||
|
<Card href={`/posts/${slug}`} frontmatter={data} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</Main>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
{currentPage}
|
||||||
|
{totalPages}
|
||||||
|
prevUrl={`/tags/${tag}${
|
||||||
|
currentPage - 1 !== 1 ? "/" + (currentPage - 1) : ""
|
||||||
|
}`}
|
||||||
|
nextUrl={`/tags/${tag}/${currentPage + 1}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Footer noMarginTop={totalPages > 1} />
|
||||||
|
</Layout>
|
||||||
@@ -4,7 +4,7 @@ import Posts from "@layouts/Posts.astro";
|
|||||||
import PostDetails from "@layouts/PostDetails.astro";
|
import PostDetails from "@layouts/PostDetails.astro";
|
||||||
import getSortedPosts from "@utils/getSortedPosts";
|
import getSortedPosts from "@utils/getSortedPosts";
|
||||||
import getPageNumbers from "@utils/getPageNumbers";
|
import getPageNumbers from "@utils/getPageNumbers";
|
||||||
import { SITE } from "@config";
|
import getPagination from "@utils/getPagination";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
post: CollectionEntry<"blog">;
|
post: CollectionEntry<"blog">;
|
||||||
@@ -32,26 +32,10 @@ const posts = await getCollection("blog");
|
|||||||
|
|
||||||
const sortedPosts = getSortedPosts(posts);
|
const sortedPosts = getSortedPosts(posts);
|
||||||
|
|
||||||
const totalPages = getPageNumbers(sortedPosts.length);
|
const pagination = getPagination({
|
||||||
|
posts: sortedPosts,
|
||||||
const currentPage =
|
page: slug,
|
||||||
slug && !isNaN(Number(slug)) && totalPages.includes(Number(slug))
|
});
|
||||||
? Number(slug)
|
|
||||||
: 0;
|
|
||||||
const lastPost = currentPage * SITE.postPerPage;
|
|
||||||
const startPost = lastPost - SITE.postPerPage;
|
|
||||||
|
|
||||||
const paginatedPosts = sortedPosts.slice(startPost, lastPost);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{
|
{post ? <PostDetails post={post} /> : <Posts {...pagination} />}
|
||||||
post ? (
|
|
||||||
<PostDetails post={post} />
|
|
||||||
) : (
|
|
||||||
<Posts
|
|
||||||
posts={paginatedPosts}
|
|
||||||
pageNum={currentPage}
|
|
||||||
totalPages={totalPages.length}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
---
|
---
|
||||||
import { SITE } from "@config";
|
import { getCollection } from "astro:content";
|
||||||
import Posts from "@layouts/Posts.astro";
|
import Posts from "@layouts/Posts.astro";
|
||||||
import getSortedPosts from "@utils/getSortedPosts";
|
import getSortedPosts from "@utils/getSortedPosts";
|
||||||
import getPageNumbers from "@utils/getPageNumbers";
|
import getPagination from "@utils/getPagination";
|
||||||
|
|
||||||
import { getCollection } from "astro:content";
|
|
||||||
|
|
||||||
const posts = await getCollection("blog");
|
const posts = await getCollection("blog");
|
||||||
|
|
||||||
const sortedPosts = getSortedPosts(posts);
|
const sortedPosts = getSortedPosts(posts);
|
||||||
|
|
||||||
const totalPages = getPageNumbers(sortedPosts.length);
|
const pagination = getPagination({
|
||||||
|
posts: sortedPosts,
|
||||||
const paginatedPosts = sortedPosts.slice(0, SITE.postPerPage);
|
page: 1,
|
||||||
|
isIndex: true,
|
||||||
|
});
|
||||||
---
|
---
|
||||||
|
|
||||||
<Posts posts={paginatedPosts} pageNum={1} totalPages={totalPages.length} />
|
<Posts {...pagination} />
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
---
|
|
||||||
import { type CollectionEntry, getCollection } from "astro:content";
|
|
||||||
import Layout from "@layouts/Layout.astro";
|
|
||||||
import Main from "@layouts/Main.astro";
|
|
||||||
import Header from "@components/Header.astro";
|
|
||||||
import Footer from "@components/Footer.astro";
|
|
||||||
import Card from "@components/Card";
|
|
||||||
import getUniqueTags from "@utils/getUniqueTags";
|
|
||||||
import getPostsByTag from "@utils/getPostsByTag";
|
|
||||||
import { SITE } from "@config";
|
|
||||||
import getSortedPosts from "@utils/getSortedPosts";
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
post: CollectionEntry<"blog">;
|
|
||||||
tag: string;
|
|
||||||
tagName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const posts = await getCollection("blog");
|
|
||||||
|
|
||||||
const tags = getUniqueTags(posts);
|
|
||||||
|
|
||||||
return tags.map(({ tag, tagName }) => {
|
|
||||||
return {
|
|
||||||
params: { tag },
|
|
||||||
props: { tag, tagName },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tag, tagName } = Astro.props;
|
|
||||||
|
|
||||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
|
||||||
|
|
||||||
const tagPosts = getPostsByTag(posts, tag);
|
|
||||||
|
|
||||||
const sortTagsPost = getSortedPosts(tagPosts);
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
|
|
||||||
<Header activeNav="tags" />
|
|
||||||
<Main
|
|
||||||
pageTitle={[`Tag:`, `${tagName}`]}
|
|
||||||
titleTransition={tag}
|
|
||||||
pageDesc={`All the articles with the tag "${tagName}".`}
|
|
||||||
>
|
|
||||||
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
sortTagsPost.map(({ data, slug }) => (
|
|
||||||
<Card href={`/posts/${slug}`} frontmatter={data} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</Main>
|
|
||||||
<Footer />
|
|
||||||
</Layout>
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import TagPosts from "@layouts/TagPosts.astro";
|
||||||
|
import getUniqueTags from "@utils/getUniqueTags";
|
||||||
|
import getPostsByTag from "@utils/getPostsByTag";
|
||||||
|
import getPageNumbers from "@utils/getPageNumbers";
|
||||||
|
import getPagination from "@utils/getPagination";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: CollectionEntry<"blog">;
|
||||||
|
tag: string;
|
||||||
|
tagName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection("blog");
|
||||||
|
|
||||||
|
const tags = getUniqueTags(posts);
|
||||||
|
|
||||||
|
return tags.flatMap(({ tag, tagName }) => {
|
||||||
|
const tagPosts = getPostsByTag(posts, tag);
|
||||||
|
const totalPages = getPageNumbers(tagPosts.length);
|
||||||
|
|
||||||
|
return totalPages.map(page => ({
|
||||||
|
params: { tag, page },
|
||||||
|
props: { tag, tagName },
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page } = Astro.params;
|
||||||
|
const { tag, tagName } = Astro.props;
|
||||||
|
|
||||||
|
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||||
|
|
||||||
|
const postsByTag = getPostsByTag(posts, tag);
|
||||||
|
|
||||||
|
const pagination = getPagination({
|
||||||
|
posts: postsByTag,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<TagPosts {...pagination} {tag} {tagName} />
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import TagPosts from "@layouts/TagPosts.astro";
|
||||||
|
import getPostsByTag from "@utils/getPostsByTag";
|
||||||
|
import getPagination from "@utils/getPagination";
|
||||||
|
import getUniqueTags from "@utils/getUniqueTags";
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = await getCollection("blog");
|
||||||
|
|
||||||
|
const tags = getUniqueTags(posts);
|
||||||
|
|
||||||
|
return tags.map(({ tag, tagName }) => {
|
||||||
|
return {
|
||||||
|
params: { tag },
|
||||||
|
props: { tag, tagName, posts },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag, tagName, posts } = Astro.props;
|
||||||
|
|
||||||
|
const postsByTag = getPostsByTag(posts, tag);
|
||||||
|
|
||||||
|
const pagination = getPagination({
|
||||||
|
posts: postsByTag,
|
||||||
|
page: 1,
|
||||||
|
isIndex: true,
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<TagPosts {...pagination} {tag} {tagName} />
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { SITE } from "@config";
|
||||||
|
import getPageNumbers from "./getPageNumbers";
|
||||||
|
|
||||||
|
interface GetPaginationProps<T> {
|
||||||
|
posts: T;
|
||||||
|
page: string | number;
|
||||||
|
isIndex?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPagination = <T>({
|
||||||
|
posts,
|
||||||
|
page,
|
||||||
|
isIndex = false,
|
||||||
|
}: GetPaginationProps<T[]>) => {
|
||||||
|
const totalPagesArray = getPageNumbers(posts.length);
|
||||||
|
const totalPages = totalPagesArray.length;
|
||||||
|
|
||||||
|
const currentPage = isIndex
|
||||||
|
? 1
|
||||||
|
: page && !isNaN(Number(page)) && totalPagesArray.includes(Number(page))
|
||||||
|
? Number(page)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const lastPost = isIndex ? SITE.postPerPage : currentPage * SITE.postPerPage;
|
||||||
|
const startPost = isIndex ? 0 : lastPost - SITE.postPerPage;
|
||||||
|
const paginatedPosts = posts.slice(startPost, lastPost);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalPages,
|
||||||
|
currentPage,
|
||||||
|
paginatedPosts,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPagination;
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { slugifyAll } from "./slugify";
|
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import getSortedPosts from "./getSortedPosts";
|
||||||
|
import { slugifyAll } from "./slugify";
|
||||||
|
|
||||||
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
|
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
|
||||||
posts.filter(post => slugifyAll(post.data.tags).includes(tag));
|
getSortedPosts(
|
||||||
|
posts.filter(post => slugifyAll(post.data.tags).includes(tag))
|
||||||
|
);
|
||||||
|
|
||||||
export default getPostsByTag;
|
export default getPostsByTag;
|
||||||
|
|||||||
Reference in New Issue
Block a user