Skip to content

Implement Pagination in AstroPaper Category Posts Page

Published: at 01:00 PM
Time to read: 5 min
Author: Mouad Nassri
Featured image for Add Pagination to AstroPaper Categories

Outlines

  1. Introduction
  2. Apply pagination
  3. Handle pagination routes
  4. Explaination
  5. Handle Breadcrumbing
  6. Source code
  7. 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.

---
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:

---
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:

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:

A screenshot of AstroPaper breadcrumb 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:

---
// 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 HERE

7. 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 !