Copy hi@ste․digital
Posted on

Super Simple Server Side Pagination in Next.js

With the introduction of the App router and server components in Next.js 13, it's now super simple to create an indexable pagination system with the help of URL parameters.

For a demonstration of this functionality, simply see this very blog! If you go to the main blog listing, you'll see the first 12 posts, with a 'next' button that will take you to page 2 of the blog, showing the next 12 posts, etc. and so on.

So let's get to it!

Querying a range of posts

First of all, we need to query our database for our posts. My posts are stored in Sanity CMS so that's what I'll be using for this example, but the query should hopefully be fairly similar whatever database/CMS you're using.

In this query, instead of getting all posts, we need to specify the range of posts we want to fetch; for example, posts 1 to 10.

The following groq query would get the first 10 posts.

export async function getPosts() {
  return client.fetch(
    groq`*[_type == "post"] | order(publishedAt desc)[0...10]{
      _id,
      publishedAt,
      title
    }`
  )
}

This is fine for our first page, but we need to make those values dynamic so we can get the correct range of posts for pages 2, 3 and so on. So we need to add two arguments to the function for start and end, and then use those values within the function in place of the static values.

export async function getPosts(start, end) {
  return client.fetch(
    groq`*[_type == "post"] | order(publishedAt desc)[${start}...${end}]{
      _id,
      publishedAt,
      title,
    }`
  )
}

Now we need to pass the appropriate start/end values through to this function for each page. So over to our blog listing page template...

Preparing the blog listing for pagination

Without pagination, we would simply import that getPosts() function and then store the results in a variable.

import { getPosts } from '@/sanity/sanity-utils'

export default async function BlogIndex() {

  const posts = await getPosts();

  return (
    <div>

      {posts.map(post => (
        <h2>{post.title}</h2>
      ))}

    </div>
  )
}

Now, we need to pass through the appropriate $start and $end arguments to the function. We know that for the first page this would simply be:

const posts = await getPosts(0, 10);

To calculate these arguments based on the current page, first of all let's set up the variables we will need.

const limit = 10; // number of posts on each page
const pageNum = 1; // the current page number

Using URL Parameters to get the current page number

Now we need that pageNum variable to change based on the current page number. We will achieve this using URL query string parameters, which Next.js makes available to your page component.

It makes sense to call our parameter 'page', so our blog pages will take the following format:

  • /blog
  • /blog?page=2
  • /blog?page=3
  • etc.

We can get access to the value of this 'page' parameter using Next.js's searchParams prop.

import { getPosts } from '@/sanity/sanity-utils'

export default async function BlogIndex({searchParams}) {

  const limit = 10;
  const pageNum = Number(searchParams?.page) || 1;

  const posts = await getPosts(0,10);

  return (
    <div>

      {posts.map(post => (
        <h2>{post.title}</h2>
      ))}

    </div>
  )
}

Here, we are checking if searchParams.page has a value, ensuring this is a Number type, and then applying this value to the pageNum variable. And if the page parameter isn't set on the URL, we must be on the main blog page, so the pageNum variable is set to 1.

The next step is to use the limit and pageNum variables to calculate the appropriate start and end values to send through to the getPosts function.

const posts = await getPosts((pageNum-1)*limit, limit*pageNum);

Let's break down this calculation so we can understand what's happening here:

Page 1
// Start: (1-1)*10 = 0
// End: 10*1 = 10

// Equivalent to:
const posts = await getPosts(0, 10); // first 10 posts
Page 2
// Start: (2-1)*10 = 10
// End: 10*2 = 20

// Equivalent to:
const posts = await getPosts(10, 20); // second 10 posts
Page 3
// Start: (3-1)*10 = 20
// End: 10*3 = 30

// Equivalent to:
const posts = await getPosts(20, 30); // third 10 posts

And just like that our pagination is in place! The main blog listing should now display the first 10 posts, /blog?page=2 should show posts 11-20, and so on.

Adding Next and Previous Buttons

The final step is to add next and previous buttons, so the user can navigate between the blog pages. This is fairly straightforward, again using the current page number to output the appropriate href values.

{pageNum !== 1 && (
  <Link
    href={pageNum === 2 ? '/blog' : {
      pathname: '/blog',
      query: {page: pageNum > 1 ? pageNum - 1 : 1 }
    }}
  >
    Previous
  </Link>
)}

{posts.length === limit && (
  <Link
    href={{
      pathname: '/blog',
      query: { page: pageNum + 1 }
    }}
  >
    Next
  </Link>
)}

To break down what's happening here, for the Previous button, we only want to display this when not on page 1. If we're on page 2, just output '/blog' as the href value. Otherwise, we can pass an object which takes a pathname and query value. The pathname is '/blog', and for the query, we set page to pageNum - 1, so on page 3 for example it would link to /blog?page=2.

For the Next button, it's a similar story. Simply set the page parameter to pageNum + 1 this time, so on page 3 for example it would link to /blog?page=4.

And there we have it! Simple, server-side pagination with physical, shareable URLs.