Outlines
- Introduction
- Apply pagination
- Handle pagination routes
- Explaination
- Handle Breadcrumbing
- Source code
- Conclusion
Introduction
In this article, we will see how to handle pagination in category posts page (categories index page) within AstroPaper theme in order to improve performence and enhance the user experience.
A quick note
Please notice that this tutorial, is a continuation of setting categories within AstroPaper theme tutorials, and in case you didn’t handle categories within your blog website yet, please go back to our guide on how to configure categories within AstroPaper theme before continue in this tutorial.
2. Apply pagination
In order to apply the pagination, we need to use a util function called getPagination inside our categories index page, and handle the routes of pagination.
-
Go to src/pages/categories/[slug]/index.astro
-
Past the following code in place of the exisiting code in that file:
---
import { getCollection } from "astro:content";
import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import { SITE } from "@config";
import getUniqueCategories from "@utils/getUniqueCategories";
import Card from "@components/Card";
import Pagination from "@components/Pagination.astro";
import getPagination from "@utils/getPagination";
export const getStaticPaths = async () => {
const categories = await getUniqueCategories();
// First define routes for categories normally
const categoriesPaths = categories.map(category => ({
params: { slug: category.slug },
props: { category: category.category },
}));
return [...categoriesPaths];
};
const { slug } = Astro.params;
const { category } = Astro.props;
const posts = await getCollection("blog");
const categoryPosts = posts.filter(post =>
post.data.categories.includes(category)
);
const { paginatedPosts, totalPages, currentPage } =
getPagination({
posts: categoryPosts,
page: 1,
isIndex: true,
});
---
<Layout title={`${category} | ${SITE.title}`}>
<Header activeNav="categories" />
<Main
pageTitle={category + "'s posts"}
pageDesc={`All posts within "${category}" category`}
>
<ul>
{
paginatedPosts.map(({ data, slug }) => (
<Card
href={`/posts/${slug}/`}
frontmatter={data}
/>
))
}
</ul>
<Pagination
{currentPage}
{totalPages}
prevUrl={`/categories/${slug}/${currentPage - 1 !== 1 ? "/" + (currentPage - 1) : ""}/`}
nextUrl={`/categories/${slug}/${currentPage + 1}/`}
/>
</Main>
<Footer />
</Layout>
Here instead of getting and looping through all category posts, we use getPagination util function to paginate the result (paginatedPosts), and get currentPage and totalPages from it, and pass them to Pagination component to show pagination buttons at the bottom of the page.
3. Handle pagination routes
Now that we paginate the category posts, and we show pagination buttons at the bottom, we need to handle routes for pagination links like when the user click next, he will be redirected to /categories/slug/2, but we do not handle these yet.
In order to handle routes for pagination links, follow with me:
-
Go to src/pages/categories/[slug], and create a file called [page].astro
-
Open this file, and past the following code:
---
import Card from "@components/Card";
import Footer from "@components/Footer.astro";
import Header from "@components/Header.astro";
import Pagination from "@components/Pagination.astro";
import { SITE } from "@config";
import Layout from "@layouts/Layout.astro";
import Main from "@layouts/Main.astro";
import getPageNumbers from "@utils/getPageNumbers";
import getPagination from "@utils/getPagination";
import { slugifyStr } from "@utils/slugify";
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
export const getStaticPaths = (async () => {
// I. Handle categories paths
const posts = await getCollection("blog");
// 1. First we get the number of posts within each category
const categoriesStats: {
category: string;
slug: string;
count: number;
}[] = [];
posts.forEach(post => {
post.data.categories.forEach(category => {
const slug = slugifyStr(category);
const record = categoriesStats.find(
c => c.category === category
);
if (record) {
record.count++;
} else {
categoriesStats.push({ category, slug, count: 1 });
}
});
});
// 2. Fedine routes for pagination
const paginationPaths = categoriesStats
.map(categoryStats => {
return getPageNumbers(categoryStats.count).map(
value => ({
params: {
slug: `${categoryStats.slug}`,
page: value,
},
props: { category: categoryStats.category },
})
);
})
.flat();
return [...paginationPaths];
}) satisfies GetStaticPaths;
const { slug, page } = Astro.params;
const { category } = Astro.props;
const posts = await getCollection("blog");
const { paginatedPosts, totalPages, currentPage } =
getPagination({
posts: posts.filter(p =>
p.data.categories.includes(category)
),
page,
});
---
<Layout title={`${category} | ${SITE.title}`}>
<Header activeNav="categories" />
<Main
pageTitle={category + "'s posts"}
pageDesc={`All posts within "${category}" category`}
>
<ul>
{
paginatedPosts.map(({ data, slug }) => (
<Card
href={`/posts/${slug}/`}
frontmatter={data}
/>
))
}
</ul>
<Pagination
{currentPage}
{totalPages}
prevUrl={`/categories/${slug}/${currentPage - 1 !== 1 ? "/" + (currentPage - 1) : ""}/`}
nextUrl={`/categories/${slug}/${currentPage + 1}/`}
/>
</Main>
<Footer />
</Layout>
4. Explainations
Here, basically, what we did is simple; We used getStaticPaths function to handle routes for our categories pagination links.
We fetched all the posts within the blog and store them in a constant called posts.
Then we define an array of objects, each object will represent the category by storing its name, slug and the number of posts present within it. We need this to handle routes for each category pagination page.
Then we loop through each post and we get its categories, and for each category we either store it in the array in case it is new, otherwise we increment the count (number of posts).
Then we map this array of categories objects and converting it to a static paths using a helper method called getPageNumbers to generate routes based on the number of pages within the category.
If for example a category called Web Developement contains 20 posts and we have 10 posts per page, we need to handle 2 paths:
-
/categories/web-developement/1
-
/categories/web-developement/2
If the user only access /categories/web-developement, it will be automatically display the first page (/categories/web-developement/1)
Once we map all of these to an array of objects with params and props, we return this array from getStaticPaths to apply those paths.
Please notice that you can change the number of posts per page, by going to src/config.ts file, and search for a property called postPerPage which is responsible for the number of posts per page in pagination.
5. Handle Breadcrumbing
Breadcrumb is a menu shown in the top of the page, to show the visitor where he is and his location on the website. The following screenshot show how AstroPaper display this menu:
Right now, If you have a categories with multiple pages, you will end up with a breadcrumb like following:
Home > Categories > [category] > [page]
But what we need to achieve is the following:
Home > Categories > [category] (page number)
To achieve this, apply the following instructions:
-
go to src/components/Breadcrumbs.astro
-
Add the following code in the frontmatter between tags and posts like following:
---
// Remove current url path and remove trailing slash if exists
const currentUrlPath = Astro.url.pathname.replace(/\/+$/, "");
// Get url array from path
// eg: /tags/tailwindcss => ['tags', 'tailwindcss']
const breadcrumbList = currentUrlPath.split("/").slice(1);
// if breadcrumb is Home > Posts > 1 <etc>
// replace Posts with Posts (page number)
breadcrumbList[0] === "posts" &&
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
// if breadcrumb is Home > Categories > [category] > [page] <etc>
// replace [[category] > [page]] with [[category] (page number)]
breadcrumbList[0] === "categories" && // Add only these lines (11 lines) to handle categories
breadcrumbList.length > 1 &&
breadcrumbList.splice(
1,
3,
`${breadcrumbList[1][0].toUpperCase() + breadcrumbList[1].slice(1).replaceAll("-", " ")} ${
Number(breadcrumbList[2]) === 1 || isNaN(Number(breadcrumbList[2]))
? ""
: `(page ${breadcrumbList[2]})`
}`
);
// 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] + ")"
}`
);
---
6. Source code
In case you want to take a look at the source code and see the changes, I made a github repo which contains an example project with all the changes and improvements needed:
🔗 SOURCE CODE HERE7. Conclusion
Pagination is important to be implemented, especially within blog and e-commerce websites, tyo help your visitors have a good experience and boost the application performence and readability.
You may don’t need this feature this time if your website is a small blog and has just a few articles, but when you start to add more content, pagination will be a MUST in order to improve user experience and boost performance and effieciency.
I hope you find this article useful, and don’t forget to share it with your friends and communities to show us your support.
Happy coding !