Hello! Welcome back.
In our last lesson, we successfully built a blog listing page that fetches and displays all your posts from your content collection. While this is a great start, it presents a scalability problem: what happens when your accounting firm publishes 50 or 100 articles? Loading them all on a single page would lead to slow load times and a poor user experience, which goes against our goal of a high-performance website.
Today, we will address this directly by implementing one of the most essential features for any content-heavy website: pagination. Your goal for this lesson is to implement pagination on the blog listing page to handle numerous posts. We will use Astro's powerful, built-in pagination system to split your blog posts into separate, statically-generated pages (e.g., /blog/1, /blog/2), ensuring your site remains fast and easy to navigate, no matter how much content you add.
The "Why" and "How" of Astro Pagination
Before diving into the code, it's important to understand why Astro's approach to pagination is so effective. Many frameworks handle pagination on the client-side, fetching new data with JavaScript as the user clicks "Next". Astro, true to its static-first nature, does this at build time. It generates a separate, static HTML file for each page of your blog roll. This means near-instant page loads and excellent SEO, as every page is fully crawlable.
The article below provides a great real-world context for this, explaining the performance issues with large datasets and how Astro's built-in tools are the perfect solution.
Implementing Astro Pagination for Large Data
The article 'Implementing Astro Pagination for Large Data' from dev.to clearly explains why pagination is critical for performance and provides a fantastic summary of the tools Astro gives you.
Please read the sections 'The Problem: Heavy Data Rendering', 'Enter Astro Pagination', 'Items per Page', and 'Page Prop Explained'. Focus on the performance benefits and familiarize yourself with the properties available on the page object, especially the data and url properties.
1. Converting the Blog Index to a Paginated Route
The first step is to tell Astro that our blog index should now be a series of pages. This requires a small but significant change.
- Rename your file: In
src/pages/blog/, renameindex.astroto[page].astro.
This bracket syntax signals to Astro that this is a dynamic route that will generate multiple pages. Astro's pagination system will automatically create pages like /blog/1, /blog/2, and so on. It's also smart enough to redirect the root /blog/ path to /blog/1.
2. Using the paginate Helper in getStaticPaths
Now that we have a dynamic route, we need to tell Astro how to generate the pages. As you learned in a previous lesson, this is the job of getStaticPaths. Astro provides a special paginate function that we can use inside it.
Here's how we adapt our blog listing page:
// src/pages/blog/[page].astro
---
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostCard from '../../components/PostCard.astro';
// This function now generates the paths for each page
export async function getStaticPaths({ paginate }) {
const allPosts = await getCollection('blog');
const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// Use the paginate helper to create pages
return paginate(sortedPosts, { pageSize: 5 }); // e.g., 5 posts per page
}
// The paginated data is passed in the `page` prop
const { page } = Astro.props;
const posts: CollectionEntry<'blog'>[] = page.data;
---
<BaseLayout title="Blog" description="A collection of my thoughts and writings.">
<section class="post-list">
<h1>Our Blog</h1>
{posts.map(post => (
<PostCard post={post} />
))}
<!-- We will add pagination controls here -->
</section>
</BaseLayout>
Let's break down the changes:
- We export an
asyncfunctiongetStaticPathsthat receives apaginatefunction in its arguments. - Inside, we fetch and sort the posts just like before.
- We then
return paginate(sortedPosts, { pageSize: 5 }). This is the magic step. Astro takes our array of posts and splits it into pages, each containing 5 items. - Outside
getStaticPaths, we accessAstro.propsto get apageobject. This object contains the posts for the current page (page.data) and a wealth of metadata. - Finally, we map over
page.data(which I've aliased topostsfor clarity) to render thePostCardcomponents for the current page only.
3. Building a Reusable Pagination Component
The page prop doesn't just contain the data; it also includes URLs for the previous and next pages. We can use these to build our navigation controls. True to our goal of building a maintainable site, we will encapsulate this logic in a reusable component.
The following video provides an excellent, end-to-end demonstration of this entire process, from setting up the paginate function to creating a clean Pagination.astro component.
Astro Blog Course #11 - Pagination
The video 'Astro Blog Course #11 - Pagination' by Coding in Public offers a complete, practical guide to implementing pagination from start to finish. It covers renaming the page, using the paginate helper, and creating a reusable component for navigation.
Watch the video from 00:15 to 07:54. Pay close attention to these key steps: How the index.astro page is converted to a dynamic [page].astro page. The implementation of getStaticPaths with the paginate function. How the page prop is used to access the posts for the current page (page.data). The creation of a reusable Pagination.astro component and how page.url.prev and page.url.next are passed as props to it.
Following the pattern from the video, let's create our Pagination component.
1. Create src/components/Pagination.astro:
This component will receive the page object and render "Previous" and "Next" links if they exist.
// src/components/Pagination.astro
---
import type { Page } from 'astro';
interface Props {
page: Page;
}
const { page } = Astro.props;
---
<nav class="pagination-nav" aria-label="Blog post navigation">
{page.url.prev ? (
<a href={page.url.prev} class="nav-link prev">← Previous</a>
) : <div />}
{page.url.next ? (
<a href={page.url.next} class="nav-link next">Next →</a>
) : <div />}
</nav>
<style>
.pagination-nav {
display: flex;
justify-content: space-between;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #eee;
}
.nav-link {
font-weight: 500;
text-decoration: none;
color: var(--color-primary, #005f73);
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.nav-link:hover {
background-color: #f0f7f8;
}
</style>
Note: We use an empty <div> as a placeholder to maintain the justify-content: space-between layout even when one of the links is missing (i.e., on the first or last page). The aria-label is important for accessibility.
2. Update src/pages/blog/[page].astro to use the component:
Now, we can import and use our new component in the blog listing page.
// src/pages/blog/[page].astro
---
// ... (imports and getStaticPaths from before)
import Pagination from '../../components/Pagination.astro'; // <-- Import Pagination
const { page } = Astro.props;
const posts: CollectionEntry<'blog'>[] = page.data;
---
<BaseLayout title={`Blog - Page ${page.currentPage}`} description="A collection of my thoughts and writings.">
<section class="post-list">
<h1>Our Blog</h1>
{posts.map(post => (
<PostCard post={post} />
))}
<Pagination page={page} /> {/* <-- Use the component */}
</section>
</BaseLayout>
With these changes, your blog page now has functional pagination! If you navigate to /blog/1 (or just /blog), you should see the first five posts and a "Next" link. Clicking it will take you to /blog/2, which will show the next five posts and both "Previous" and "Next" links.
Test your understanding!
Currently, our pagination component only shows "Previous" and "Next". For better usability, let's add a "Page X of Y" indicator.
Using the page prop, which contains properties like currentPage and lastPage, modify your src/components/Pagination.astro component to display the current page number and the total number of pages between the "Previous" and "Next" links.
For example, it should look something like: ← Previous Page 2 of 5 Next →
Show answer
You would modify src/components/Pagination.astro to include a span that displays the page numbers.
// src/components/Pagination.astro
---
import type { Page } from 'astro';
interface Props {
page: Page;
}
const { page } = Astro.props;
---
<nav class="pagination-nav" aria-label="Blog post navigation">
{page.url.prev ? (
<a href={page.url.prev} class="nav-link prev">← Previous</a>
) : <div class="placeholder" />}
<span class="page-indicator">
Page {page.currentPage} of {page.lastPage}
</span>
{page.url.next ? (
<a href={page.url.next} class="nav-link next">Next →</a>
) : <div class="placeholder" />}
</nav>
<style>
.pagination-nav {
display: flex;
justify-content: space-between;
align-items: center; /* <-- Align items vertically */
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #eee;
}
.nav-link {
/* ... same styles */
}
.nav-link:hover {
/* ... same styles */
}
.page-indicator {
font-size: 0.9rem;
color: #666;
}
.placeholder {
/* A placeholder to keep the layout consistent. It needs a width. */
width: 95px; /* Adjust width to match your link's width */
}
</style>
This simple addition significantly improves the user's awareness of where they are in the content archive. It leverages the rich metadata provided by Astro's page prop without any complex logic.
Conclusion
Fantastic work! You've successfully replaced your single, monolithic blog list with a scalable, high-performance paginated archive. This is a crucial skill for building any real-world, content-driven website.
Key Takeaways:
- Astro handles pagination at build time for maximum performance and SEO benefits.
- Paginated routes are created by renaming an index file to use brackets, like
[page].astro. - The
paginate()helper function insidegetStaticPathsis the core of Astro's pagination system. - The
pageprop passed to your component contains both the data for the current page (page.data) and URLs for navigation (page.url.prev,page.url.next, etc.). - Encapsulating navigation controls into a reusable
Pagination.astrocomponent keeps your page templates clean and your codebase maintainable.
Up Next:
You've now created a base layout, dynamic routes for individual posts, a blog listing page, and pagination. In the next lesson, we will refine the dynamic page templates to render individual blog posts and service pages. We'll focus on creating a polished, readable, and professional layout specifically designed for a single piece of content, further advancing the "premium and trustworthy" design goal for your accounting website.