Proper websites, done properly

Part 8: Pagination

6 minute read time / 727 words

Once our posts index starts to grow, we need to be able to paginate our results to make them easier to browse

Pagination is a great way to divide up large numbers of content items into more digestible chunks. Astro provides some of the functionality we need out of the box with its built-in paginate() function. First, we need to rename our post listings page file from index.astro to [...page].astro, and then update it to look like this:

---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
import PostItem from '../../components/PostItem.astro';
import Pagination from '../../components/Pagination.astro';

export async function getStaticPaths({ paginate }) {
    const postEntries = await getCollection('posts', ({ data }) => {
        return data.isDraft !== true;
    });

    return paginate(postEntries, { pageSize: 1 });
}

const { page } = Astro.props;
const pathname = new URL(Astro.request.url).pathname.split('/');
const firstPath = pathname[1];
---
<Layout title={`Posts, page: ${page.currentPage}`}>
    <p>Posts {page.start + 1} to {page.end + 1}, page {page.currentPage}</p>

    {page.data.map((post) => (
        <PostItem item={post} />
    ))}

    <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}`}
    />
</Layout>

The main changes here are the inclusion of the <Pagination /> component (we'll create that in a moment!), and we're passing the results of fetching our posts collection into the pagination function and telling it we want to output one post per page.

This is just temporary so that we can test more than one page of results using our test post pages, and a more sensible number should be used in production; probably 10 or so posts should be reasonable.

The paginate functions provides us a lot of useful information, and most of this we're going to pass directly into the pagination component to let it figure out what links to output for us.

Now that we have our paginate function in place, and we technically can get to each page of posts, we're missing a way to navigate to them. Let's look to create our pagination component now we have the data we need to construct it.

---
const {
    length,
    currentUrl,
    currentPage,
    firstUrl,
    prevUrl,
    nextUrl,
    lastUrl
} = Astro.props;
---
<nav aria-label="Post pages">
    {firstUrl === currentUrl ? (
        <span class="page is_disabled" aria-hidden="true">
            First<span class="vh"> page</span>
        </span>
    ) : (
        <a href={firstUrl} class="page">
            First<span class="vh"> page of posts</span>
        </a>
    )}

    {prevUrl ? (
        <a href={prevUrl} class="page">
            Prev<span class="vh">ious page of posts</span>
        </a>
    ) : (
        <span class="page is_disabled" aria-hidden="true">
            Prev<span class="vh">ious page of posts</span>
        </span>
    )}

    {!nextUrl ? (
        <span class="page is_disabled" aria-hidden="true">
            Next<span class="vh"> page of posts</span>
        </span>
    ) : (
        <a href={nextUrl} class="page">
            Next<span class="vh"> page of posts</span>
        </a>
    )}

    {lastUrl === currentUrl ? (
        <span class="page is_disabled" aria-hidden="true">
            Last<span class="vh"> page of posts</span>
        </span>
    ) : (
        <a href={lastUrl} class="page">
            Last<span class="vh"> page of posts</span>
        </a>
    )}
</nav>

This method takes the basics from an article by Ted Krueger which implements the paginate() function and outputs first, previous, next, last and numbered pages in the pagination. I've adjusted to suit our needs, and simplified by removing the numbered pages links as the other links will be enough.

The vh class is a visually hidden screen reader text method that allows us to give additional context to users who might not be able to see our pagination and understand it the same way a sighted user might. To see this working, you can just add this CSS to your site:

We output a <span> instead of a link element when the pagination item we're outputting is the page we're currently on. There's no real reason to link to the same page we're on, so we also add the aria-hidden="true" attribute to those elements so screen readers should skip over them. These provide no help to assistive software users so there is no reason to force them to be read aloud.

.vh {
    position: absolute;
    overflow: hidden;
    clip: rect(0 0 0 0);
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
}

This is all we need for a basic pagination implementation, so give it a try. Add more posts, change up the per page value and see what suits you.