Creating A Pagination Component With Astro

Written by Ted Krueger

Here at RIMdev, we’re currently converting our static sites to use Astro. As we switch from our current SSGs, Hugo and Jekyll, we’re creating reusable components that we’d like to document for the team. This series of posts will contain those components for not only our benefit but hopefully yours as well!

Something that Hugo did well was Pagination. It was very simple to implement, but it came with some limitations. Astro does not have a specific Pagination component, but it does give you the tools to make your own. Here’s what I came up with and used on a couple of our sites for blog sections…

TLDR;

Get to the point!

First, create your blog page within /src/pages. I like to add another folder here called /blog just to try to keep things a bit cleaner. So we have:

src
- pages
  - blog

Inside here add your page for the blog landing. This is the page that will show all of your posts, or at least what we want. You’ll need to use brackets in this page title. Call it [...page].astro. We need the [] wrapping the name as this is a dynamic route.

src
- pages
  - blog
    - [...page].astro

Blog Landing

Now let’s get to the guts of [...page].astro. You’ll be leveraging page.data here. We also need 2 other components: PostSummary.astro, which is a block of blog info like a title, image, and a teaser. You can add more to this if you need, and I’ll show you what this looks like in a bit. We also need a Pagination.astro component. This can also be customized to your needs, but it will most likely always have the same data passed to it.

The frontmatter of [...page].astro should look like:

---
import PostSummary from "../../layouts/partials/blog/PostSummary.astro";
import Pagination from '../../components/Pagination.astro';

export async function getStaticPaths({ paginate }) {
  const allPosts = await Astro.glob('./*.md*');
  const formattedPosts = allPosts.sort((a, b) => new Date(a.frontmatter.date) - new Date(b.frontmatter.date)).reverse();

  return paginate(formattedPosts, {
    pageSize: 5,
  })
}
const { page } = Astro.props;
---

Include your components, PostSummary and Pagination. We use the getStaticPaths function to grab each blog post. In this case, it’s anything in our /blog folder, and we’re looking for any file that ends in either a .md or .mdx file type. We want to format them by the most recent post. That’s what we’re doing with the formattedPosts variable. Once we have our posts array, we can set the total number of posts we want to show. In this case, it’s 5.

  return paginate(formattedPosts, {
    pageSize: 5,
  })

Finally we need to get page data so we use:

const { page } = Astro.props;

If you want to see what this looks like, add console.log(page) in your front matter. This will show you an array of your posts and the data inside them that you can utilize.

Populate The Posts

Using page we can map the array:

{
    page.data.map((post) => (
        <PostSummary 
            url={post.slug} 
            title={post.data.title} 
            thumbnail={post.data.thumbnail} 
            summary={post.data.summary} 
        />
    ))
}

When populated, the PostSummary looks like this:

Example of a rendered PostSummary.

So a PostSummary is being rendered for each post, but it’s only up to 5 since that’s what we set the pagination limit to.

Let’s add the pagination underneath the posts. Using our Pagination component:

<Pagination 
    length={page.lastPage} 
    currentUrl={page.url.current} 
    currentPage={page.currentPage} 
    firstUrl={`/${firstPath}`} 
    prevUrl={page.url.prev} 
    nextUrl={page.url.next} 
    lastUrl={`/${firstPath}/${page.lastPage}`}
/>

PostSummary Component

Let’s take a look at each component starting with PostSummary.astro:

---
const { title, url, thumbnail, summary } = Astro.props;
---

<div class="block">
  <div class="block-container blocks px-3 post-summary">
    <div class="block tablet-up-4 lg-tablet-up-3">
      <img src={`/images/blog/${thumbnail}`} alt={title}>
    </div>
    <div class="block tablet-up-8 lg-tablet-up-9">
      <a href={`/blog/${url}`}>
        <h2 class="heading">{title}</h2>
      </a>
      <p>{summary}</p>
      <a href={`/blog/${url}`} class="post-summary__action text--bold">Read More</a>
    </div>
  </div>
</div>

Pagination Component

Now Pagination.astro:

---
const { length, currentUrl, currentPage, firstUrl, prevUrl, nextUrl, lastUrl } = Astro.props;
const paginationList = Array.from({length: `${length}`}, (_, i) => i + 1);
---

<nav aria-label="Blog pages" class="pagination pagination-default">
    { firstUrl == currentUrl ? (
        <span class="pagination__link disabled">
            <i class="pi-angle-left"></i>
            <i class="pi-angle-left"></i>
        </span>
    ) : (
        <a href={firstUrl} class="pagination__link">
            <i class="pi-angle-left"></i>
            <i class="pi-angle-left"></i>
        </a>
    )}

    { prevUrl ? (
        <a href={prevUrl} class="pagination__link">
            <i class="pi-angle-left"></i>
        </a>
    ) : (
        <span class="pagination__link disabled">
            <i class="pi-angle-left"></i>
        </span>        
    )}
    
    {
        paginationList.map((num) => (
            <a href={`${firstUrl}${num == 1 ? "" : '/'+(num)}`} class={`pagination__link ${currentPage == num ? "disabled active" : ""}`}>
                {num}
            </a>
        ))
    }

    { !nextUrl ? (
        <span class="pagination__link disabled">
            <i class="pi-angle-right"></i>
        </span>
    ) : (
        <a href={nextUrl} class="pagination__link">
            <i class="pi-angle-right"></i>
        </a>
    )}

    { lastUrl == currentUrl ? (
        <span class="pagination__link disabled">
            <i class="pi-angle-right"></i>
            <i class="pi-angle-right"></i>
        </span>
    ) : (
        <a href={lastUrl} class="pagination__link">
            <i class="pi-angle-right"></i>
            <i class="pi-angle-right"></i>
        </a>
    )}
    
</nav>

The Pagination component should look something like this: (This has been styled)

Example of the rendered Pagination.

Putting It Together

---
import BlogLanding from "../../layouts/blog/BlogLanding.astro";

import PostSummary from "../../layouts/partials/blog/PostSummary.astro";
import Pagination from '../../components/Pagination.astro';

const allPosts = await Astro.glob('./*.md*');
export async function getStaticPaths({ paginate }) {
  const allPosts = await Astro.glob('./*.md*');
  const formattedPosts = allPosts.sort((a, b) => new Date(a.frontmatter.date) - new Date(b.frontmatter.date)).reverse();

  return paginate(formattedPosts, {
    pageSize: 5,
  })
}
const { page } = Astro.props;
const pathname = new URL(Astro.request.url).pathname.split('/');
const firstPath = pathname[1];
---
<BlogLanding frontmatter>
  <ol class="posts list">
    {
      page.data.map((post) => (
        <li class="post">
          <PostSummary 
            url={post.url} 
            title={post.frontmatter.title} 
            date={post.frontmatter.date} 
            summary={post.frontmatter.teaser} 
            tags={post.frontmatter.tags}
            categories={post.frontmatter.categories}
          />
        </li>
      ))
    }
  </ol>

  {allPosts.length > 10 ? (
    <div class="mt-4">
      <Pagination length={page.lastPage} 
        currentUrl={page.url.current} 
        currentPage={page.currentPage} 
        firstUrl={`/${firstPath}`} 
        prevUrl={page.url.prev} 
        nextUrl={page.url.next} 
        lastUrl={`/${firstPath}/${page.lastPage}`}
      />
    </div>
  ) : null}
</BlogLanding>

<style lang="scss">
  .post {

    + .post {
      margin-top: 4rem;
    }
  }
</style>

Published March 09, 2023 by

undefined avatar
Ted Krueger Github UI/UX Developer

Suggested Reading