How to implement Next.js pagination: step-by-step tutorial with code examples

Updated on June 18, 2025

·

Originally published on April 23, 2021

How to implement Next.js pagination: Step-by-step tutorial with code examples

Pagination in web development means dividing your data (usually content, like blog posts or recipes) into smaller chunks (pages) and loading them one page at a time, improving initial page load times. 

In this post, you'll learn how to add pagination to your Next.js project, using data fetched from a GraphQL API. You'll also learn how to add pagination controls to your UI (for example, next/previous buttons).

What is static pagination in Next.js?

This tutorial follows a Next.js static pagination approach, which means all pages are pre-rendered into static HTML at build time. This is a great approach because it requires no client-side state — pages load super fast, yet the server load is minimal as it's just loading static files. And, as the pages take no time to load, they can be more easily crawled for SEO purposes.

In this tutorial, you'll learn how to use Next.js with App Router to create a short blog for hosting meal recipes, and then you'll add static pagination to it. You'll also learn how to fetch data from a GraphQL API, using Contentful to manage your content. All the code in this tutorial is available in our GitHub repository.

Creating content for pagination

We’re going to briefly run through  this because the Contentful CMS is easy to sign up for, use, and manage. As you'll be creating a recipe blog, you'll need to create a Recipe content model and then add just enough recipe items to fill up multiple pages — three recipes will work fine for example purposes.

So, head to https://www.contentful.com/sign-up/ and sign up, then just follow these easy steps in the Contentful app:

  1. Click on Content model (top left) to jump into the Content Modeler and click on + Create content type.

  2. We’ll only need one content type, so enter Recipe for the name and click Create.

  3. Next, click + Add field. To keep things simple, just add a title by choosing Text.

  4. Now enter Title to set the name and field ID.

  5. Make sure the Short text option is selected.

  6. Click Add and configure.

  7. Under Settings, select This field represents the entry title — this just helps you find each entry more easily later.

  8. Under Validation, select Required field.

  9. Finally, click Confirm.

Now you've created your content model. Next, you need to add a few different entries — i.e., recipes! To add a recipe, click Content (top left, again), then click Add entry (top right) and select Recipe. 

  1. Enter a title — e.g., "Spaghetti Bolognese."

  2. Click Publish.

  3. Click Content (top left).

Add at least three different recipes so you have enough content to paginate.

Adding your Contentful API keys

You'll need an API key for your code to make requests to the Contentful GraphQL API later, so here’s how to generate one:

  1. Click the settings cog icon, at the top right next to your profile icon.

  2. Click API keys.

  3. Click + Add API key.

  4. Enter a name and click Add API key.

Copy the Space ID and Content Delivery API - access token values and store them somewhere safe. Later, you’ll be using them to query your content in the Contentful interactive GraphQL explorer and also add them to your .env.local file.

Starting up a Next.js 13+ App Router project

First, to scaffold a basic Next.js app, run this command: npx create-next-app@latest nextjs-graphql-paging-intro

When the response gives you options, be sure to select these options you see below (just press Return or Enter for the last one):

Now run npm run dev to start up the app, and you can access it at https://localhost:3000 in your browser.

Querying paginated data from a GraphQL API

Most GraphQL APIs support pagination, and Contentful's API is no different. This means you can retrieve your data in chunks that are the same size as the number of items you want to display per page. The following query can be used to retrieve the data needed for your first page of recipes:

recipeCollection is a collection query that takes parameters allowing you to specify the size of a returned page of recipe items, and how many to skip before returning them — this is the core of paging in our app.

In the above query, limit is set as 2. You should set the limit as your page size — i.e., the number of items you want to display per page. The skip parameter means "skip the first N items." You can use this to paginate your data if you always set its value as either zero (for the first page) or a multiple of your page size number. For example, to load the second page of recipe data, you'd use limit: 2, skip: 2.

The retrieved properties (in this case the system id and title) are defined in the query to provide your app with properties from your retrieved items. id is a system-defined value, which we can ignore for now, but can be useful for building specific item requests later. title is the title of the recipe, which you defined earlier when creating the content model.

You'll need to embed your GraphQL query into your Next.js code, but it can be a good idea to run the query first in Contentful's GraphiQL tool, to ensure it's returning the correct result. To do this, simply open the GraphiQL app for your Contentful account by pasting this URL into your browser:

https://graphql.contentful.com/content/v1/spaces/{SPACE}/explore?access_token={CDA_TOKEN}

Replace {SPACE} with the Space ID, which you copied earlier, and replace {CDA_TOKEN} with the Content Delivery API - access token (also copied earlier).

Paste the GraphQL query above into the left pane and click the pink run button and it should return a result like this:

nextjs-pagination-image4

Fetching your paginated content in Next.js

Now you're ready to add the GraphQL query to your Next.js code. First, put the following content into the file src/lib/Config.js, which tells Next.js how large a page will be. Assuming you’ve got a minimum of three recipes in your content (created above), let’s keep the page size to only 2.

Next, create the src/lib/ContentfulApi.js file and import the Config.js file.

Below this, declare a class with export default class ContentfulApi and drop in this getPaginatedPostSummaries(page) function. Your file should look like this:

The replacement of limit and skip parameters is performed with string interpolation and this simply passes in the pagination values, discussed above, to the GraphQL query.

To actually make a call to the Contentful GraphQL API we will need one more function. You’ll notice we’re missing the callContentful(query) function, so let’s add it to the ContentfulApi class in the ContentfulApi.js file now.

This is defining the URL to be used and adding the Space ID that you retrieved from the API key earlier. It then builds the request header and adds in the access token, also from the API key, along with the query in the request body. The function then returns the content retrieved from GraphQL.

To add the Space ID and access token, create a file in the root directory of the project called .env.local and add this content:

CONTENTFUL_SPACE_ID=<your space id>

CONTENTFUL_ACCESS_TOKEN=<your access token>

As mentioned above, paste in your Space ID and access token and save it.

Building Next.js components for a list of recipes and for pagination

Implementing a blog post list component for your recipes

Displaying the list of recipe posts requires a component. We’ll call this PostList.js and add it here.

Here, you’re importing the Next.js Link component, which makes navigation easy. This will provide a link you can populate later to navigate to the individual recipes. The Pagination component is also imported, but we’ll come to that later.

The props we’re interested in are the posts retrieved from GraphQL, the current page number, and the total number of pages, as integers.

The prevDisabled and nextDisabled variables are booleans that show whether we should be linking to the previous and/or next page of results, respectively. These are passed to the forthcoming Pagination component so it doesn’t need to work them out itself (we already have the information).

Then we use the map() iterator to loop over the items for this page and render them as <li> elements. No styling yet, but we’ll come to that, too.

Finally, the <Pagination> element will show the pagination controls.

Implementing the Pagination component

Now we come to the real Pagination component, which handles the navigation between pages. Drop this into the new file Pagination.js:

Again, the Next.js Link component is imported to create the next and previous page links.

The values totalPages, currentPage, prevDisabled and nextDisabled are destructured from the props, passed in from the PostList component, as you saw above.

The prevPageUrl and nextPageUrl values are built as strings and used to provide the previous and next links, either side of the current page number.

Then the previous page link is built using a <li> element. If the current page is page 1 then prevDisabled will be false and no link is rendered.

The text between the previous and next links will read “Page 1 of 2,” for example, and will dynamically change to reflect the current and total number of pages.

The third <li> element renders the next page link if there is one.

Dynamically build the Next.js routes for your paginated pages

Create the file src/app/recipe/[page]/page.js. This is an important path name and deserves explanation.

The Next.js App Router will automatically route requests to any URL under /recipe to the page.js inside the /recipe/[page] folder. The [page] folder is named using square brackets to tell the App Router that it’s not part of the URL, but a placeholder for the page index.

If there’s a page.js directly within the /recipe folder (at the same level as the [page] folder) it would handle requests at the /recipe level. But we’ll come to that shortly.

Here, we’re importing the ContentfulApi class and PostList component, both built earlier, and also Config.js, which defines how many items on a page — the page size, if you will.

The generateStaticParams function provides the list of statically rendered paths to Next.js so that those paths can be pre-rendered at build time. This is built using the paths list.

RecipeIndexPage provides the content for the individual pages of paginated content and is retrieved as a template when Next.js pre-renders the static content.

The await params is required to allow the asynchronous code that’s generating the static content to fully receive the page number from the static generation logic in Next.js. Without this, the params may not be fully populated and throw an error. (You’ll actually see a build time error in the console when you try to run it without the await.)

getPaginatedPostSummaries retrieves the GraphQL response, which lists the summaries of the posts for this page, and totalPages calculates the number of pages to be linked by the Pagination component.

Finally, everything is passed to the PostList component for rendering.

If you haven’t done so already, run this whole thing with npm run dev and load it up in your browser by navigating to http://localhost:3000/recipe/1. We will add some styling later.

nextjs-pagination-image6

Handling the root /recipe URL

Now, you might notice that if you navigate to http://localhost:3000/recipe you get an error message. This is because app routing in Next is not finding a page at src/app/recipe/page.js, so just drop this file in and any request to http://localhost:3000/recipe will be automatically forwarded to http://localhost:3000/recipe/1.

Styling your Next.js pagination components

So far, the presentation has been a little bare. This is intentional, so that it doesn’t conflict with your own styling, but let’s add something to globals.css to make it a little more aesthetically pleasing. Add this at the end of your globals.css file, then we’ll take a look at the elements to be updated.

Now, open src/components/PostList.js and change <ol> to <ol className="postlist">, then open src/components/Pagination.js and change <ol> to <ol className="pagination">.

Now, check the page and you should see something more like this (refresh the page if not):

nextjs-pagination-image7

It’s not glorious, but it’ll get you started.

Your own solution

Using this tutorial you built a simple recipe site using the powerful static site rendering built into Next.js, powered it with Contentful’s GraphQL data, and added Next.js pagination. Contentful makes it easy to create a content model and publish quickly. Your Next.js static site will host your content from a CDN without needing a full site rebuild. This is great for SEO and saving time.

Static sites aren't the only approach, though. If you have a lot of fast-changing content, then you'd need to rebuild frequently to keep the content fresh. In that situation, you might use server-side rendering, even though the page doesn't load as fast, or ISR, which is a mix of the static site approach and SSR. 

Contentful also has the added benefit of allowing non-developers, like marketing professionals, to create and update content themselves. Try out Contentful today to see how it can help your Next.js site.

Subscribe for updates

Build better digital experiences with Contentful updates direct to your inbox.

Meet the authors

David Fateh

David Fateh

Software Engineer

Contentful

David Fateh is a software engineer with a penchant for web development. He helped build the Contentful App Framework and now works with developers that want to take advantage of it.

Related articles

Guides

GraphQL vs. REST: Exploring how they work

August 16, 2023

This comprehensive guide explores the key differences and possibilities of Rust and TypeScript and explains their type systems and error-handling capabilities.
Guides

Rust and TypeScript: A comprehensive guide to their differences and integration

December 4, 2024

A tutorial on GraphQL pagination, including cursor and offset-based methods. Examples integrate real-world GraphQL APIs into a React application.
Guides

GraphQL pagination: Cursor and offset tutorials

October 28, 2024

Contentful Logo 2.5 Dark

Ready to start building?

Put everything you learned into action. Create and publish your content with Contentful — no credit card required.

Get started