Introduction
Astro has a very in-depth section of the documentation devoted to Pagination. If you’re looking to do Pagination in Astro, it’s well-defined.
The documentation has a few different (but related) concepts. First, it has paths such as /astronauts/[page].astro
, where [page]
is the page number corresponding to the currentPage
of the Pagination. When I read this, it was exactly what I wanted, so I promptly added a /posts/[page].astro
.
Second, in the Static (SSG) Mode section of the docs, they use the example of /dogs/[dog].astro
where [dog]
is the page about that dog. When I read this, it was exactly what I wanted for a single post view, so I promptly added a /posts/[...slug].astro
. Honestly, I don’t know what the destructuring dots do (e.g., ...slug
). I assume it allows you multiple paths: /posts/some-other-path/my-single-post
.
Both of these pages required having:
// in [...slug].astro, no parameter used in getStaticPaths
export async function getStaticPaths() {
//... get sorted posts
return sortedPosts.map((p) => ({
params: { slug: p.slug },
}));
}
const { slug } = Astro.params;
// in [page].astro, using the `paginate()` function
// SITE is a constant that uses some configuration site-wide. I saw this in a theme, and liked the pattern.
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
//... get sorted posts
return paginate(sortedPosts, { pageSize: Number(SITE.postPerPage) });
}
// This `page` comes from the `paginate()` function, and contains some useful data:
// <https://docs.astro.build/en/reference/api-reference/#the-pagination-page-prop>
const { page } = Astro.props
So now I’ve got two pages:
[page].astro
- which will handle pagination for me (e.g.,/post/1
,/post/2
, etc.)[...slug].astro
- which will handle post pages for me (e.g.,/post/using-pagination-in-astro
)
But what if I want a third page called index.astro
where I list the first pagination page?
According to the documentation:
If a page uses dynamic params in the filename, that component will need to export a
getStaticPaths()
function.
In other words, getStaticPaths()
will never get called inside of my index.astro
or /posts
because it does not have a dynamic parameter, it is a Static Route. I don’t want to link users to /posts/1
when they visit my blog. There has to be a better way of accomplishing what I need.
Looking at the [page].astro
. It renders a Posts
component that takes in Page<CollectionEntry<'post'>>
as a prop. When getStaticPaths({paginate})
is called, it will return a page
prop of that type to my page. Then, I pass this page
variable directly to my Posts
component as a prop of the same kind. I want to reuse all this for the index.astro
page because it works well.
---
// in [page].astro, using the `paginate()` function
// SITE is a constant that uses some configuration site-wide. I saw this in a theme, and liked the pattern.
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
//... get sorted posts
return paginate(sortedPosts, { pageSize: Number(SITE.postPerPage) });
}
// This `page` comes from the `paginate()` function, and contains some useful data:
// <https://docs.astro.build/en/reference/api-reference/#the-pagination-page-prop>
const { page } = Astro.props
---
<Posts {page} />
To get index.astro
to work, I need to overcome the following issues:
- I won’t be able to use
getStaticPaths
because this is not a dynamic route. - If I can’t use
getStaticPaths
, then I can’t usepaginate()
because, from what I’ve seen, it’s a private function only available as a parameter togetStaticPaths
.
Initial Solution
I decided I was too lazy to create a specialized component that sorted my posts in the same way as posts/[page].astro
but only returned the amount found in SITE.postsPerPage
, and hacked the Pagination
component to show the next page. Whew, I get the willies and the chills just thinking about it.
Not only is it duplicating code, but a year+ from now, when I need to change anything about index.astro
, I will be perplexed that I’m rendering three different components—2 of which render the same thing—inside:
[page].astro
[...slug].astro
index.astro
So, I wanted to keep it more straightforward. Since I’m already passing page
as a prop to my Posts
component, why not “hack” that and re-use my Posts
component?
Example Component
// ... get sortedPosts
const pagination = getPagination({
items: sortedPosts,
page: 1,
isIndex: true,
});
// Example of the math that the `paginate()` function uses
// Ex (3 per page) (current page 1) = 0
// Ex (3 per page) (current page 2) = 3
// Ex (3 per page) (current page 3) = 6
const start = (pagination.currentPage - 1) * SITE.postPerPage;
// Ex (3 per page) (current page 1) = 2
// Ex (3 per page) (current page 2) = 5
// Ex (3 per page) (current page 3) = 8
const end = pagination.currentPage * SITE.postPerPage - 1;
// Ex (3 per page) (current page 1) = 4
// Ex (3 per page) (current page 1) = 4
// Ex (3 per page) (current page 3) = 4
const lastPage = pagination.totalPages;
// Ex (3 per page) (current page 1) = 3
// Ex (3 per page) (current page 2) = 3
// Ex (3 per page) (current page 3) = 3
let size = SITE.postPerPage;
// Ex (3 per page) (current page 1) = 10
// Ex (3 per page) (current page 2) = 10
// Ex (3 per page) (current page 3) = 10
const total = pagination.totalPages * SITE.postPerPage;
const page: Page<CollectionEntry<'post'>> = {
data: pagination.paginatedItems,
// Index of first item on current page, starting at 0.
// (e.g. if pageSize: 25, this would be 0 on page 1, 25 on page 2, etc.)
start,
// Index of last item on current page.
end,
// The total number of items across all pages.
total,
// The current page number, starting with 1.
currentPage: pagination.currentPage,
// How many items per-page.
size,
// The total number of pages.
lastPage,
url: {
current: '/posts/1',
prev: undefined,
next: `/posts/2`,
first: undefined,
last: undefined,
},
};
A lot is going on in this snippet. So, let me break it down.
First, we get the posts and sort them. However you want to sort your posts. By the way, for me, it looks like this:
const posts = await getCollection('post');
const featuredPost = await getCollection('post', ({ data }) => {
// only return isFeatured === true
return data.featured.isFeatured;
});
const sortedPosts = getPostsSortedByDate(posts);
But it’s better to just say //... get sortedPosts
since you can fetch posts from any source. I just happen to be using Markdown.
Next, I call a getPagination
function, which I stole from a theme called Astro Paper. The theme is excellent; if you’re looking for something quick and easy to learn Astro, I’d suggest looking at it. It taught me a lot when I was initially picking up Astro.
The getPagination
function looks like this:
interface GetPaginationProps<T> {
items: T;
page: string | number;
isIndex?: boolean;
}
const getPagination = <T>({ items, page, isIndex = false }: GetPaginationProps<T[]>) => {
const totalPagesArray = getPageNumbers(items.length);
const totalPages = totalPagesArray.length;
const currentPage = isIndex
? 1
: page && !isNaN(Number(page)) && totalPagesArray.includes(Number(page))
? Number(page)
: 0;
const lastPost = isIndex ? SITE.postPerPage : currentPage * SITE.postPerPage;
const startPost = isIndex ? 0 : lastPost - SITE.postPerPage;
const paginatedItems = items.slice(startPost, lastPost);
return {
totalPages,
currentPage,
paginatedItems,
};
};
const getPageNumbers = (numberOfPosts: number) => {
const numberOfPages = numberOfPosts / Number(SITE.postPerPage);
let pageNumbers: number[] = [];
for (let i = 1; i <= Math.ceil(numberOfPages); i++) {
pageNumbers = [...pageNumbers, i];
}
return pageNumbers;
};
You can read the JavaScript to understand what it’s doing in more detail, but basically, it does the following:
- Takes the total number of posts and chunks them according to our site setting of
postPerPage
. - Gets the Current page
- The last post number for that page
- The starting post number for that page
- Then, perform some array aerobatics to return a slice of those posts for that page.
This is essentially what the paginate()
function is doing while returning much more metadata.
I use the information returned via this function to populate the metadata for posts. There’s some math involved, but it really only needs to be good enough for the first page. Once the user clicks “Next” on your pagination, it should link them to /posts/2
, and if they click “Previous,” it should link them to /posts/1
.
Once we’re on a /posts/[page].astro
, we’ve handed it off to Astro’s built-in and excellent paginate
function.
Conclusion
I learned that getStaticPaths
is only used on pages with dynamic routes and not for static routes. An alternative must be used on a static route like index.astro
.
Since my static route only needs to be good enough to hand off to the dynamic route, I can significantly simplify this process. For example, instead of having a paginate function, what if I just did:
const posts = await getCollection('post');
const featuredPost = await getCollection('post', ({ data }) => {
return data.featured.isFeatured;
});
const sortedPosts = getPostsSortedByDate(posts);
// get rid of getPagination function
const paginatedItems = sortedPosts.slice(0, SITE.postPerPage);
const start = 0;
const end = SITE.postPerPage - 1;
const lastPage = sortedPosts.length;
let size = SITE.postPerPage;
const total = sortedPosts.length * SITE.postPerPage;
const page: Page<CollectionEntry<'post'>> = {
data: paginatedItems,
// Index of first item on current page, starting at 0.
// (e.g. if pageSize: 25, this would be 0 on page 1, 25 on page 2, etc.)
start,
// Index of last item on current page.
end,
// The total number of items across all pages.
total,
// The current page number, starting with 1.
currentPage: 1,
// How many items per-page.
size,
// The total number of pages.
lastPage,
url: {
current: '/posts/1',
prev: undefined,
next: `/posts/2`,
first: undefined,
last: undefined,
},
};
I only need the index.astro
page to be good enough to hand off to the dynamic route. Doing it this way, I can reuse my Posts
component:
<Posts {page} />
On both the index.astro
and [page].astro
, the logic to inject page
doesn’t really matter. Since it’s a component, it doesn’t care how it gets its props, just that it can use them.
Then, on the [...slug].astro
page, I render the markdown. This solution feels nice to me, and I have learned a lot about this aspect of Astro, so I thought I’d share it in this post. Hopefully, you were able to learn something, too.