Updated on June 18, 2025
·Originally published on April 23, 2021
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).
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.
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:
Click on Content model (top left) to jump into the Content Modeler and click on + Create content type.
We’ll only need one content type, so enter Recipe for the name and click Create.
Next, click + Add field. To keep things simple, just add a title by choosing Text.
Now enter Title to set the name and field ID.
Make sure the Short text option is selected.
Click Add and configure.
Under Settings, select This field represents the entry title — this just helps you find each entry more easily later.
Under Validation, select Required field.
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.
Enter a title — e.g., "Spaghetti Bolognese."
Click Publish.
Click Content (top left).
Add at least three different recipes so you have enough content to paginate.
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:
Click the settings cog icon, at the top right next to your profile icon.
Click API keys.
Click + Add API key.
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.
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.
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:
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.
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.
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.
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 /recip
e 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.
/recipe
URLNow, 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
.
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):
It’s not glorious, but it’ll get you started.
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.