DevelopmentGatsbyJS | 19 Min Read

How to Create Paginated Pages on GatsbyJS With Pagination Controls

A common solution for a blog with a higher number of posts is to paginate them. Here's how to implement page pagination with GatsbyJS blogs.

Welcome back to the blog! In this post, we are going to be looking at how to add pagination to pages on a GatsbyJS website. By the end of this post:

  1. Your website will be able to create pages as required to house all your content.
  2. The number of pages will be determined by the number of posts we want per page.
  3. Finally, we will add controls to navigate between pages with the ability to skip to a certain page.

What Are We Creating?

Below is a screenshot from my live website Today, we will be covering the functionality that allows us to specify I want X posts per page and then have Gatsby create the pages for us. For the purposes of this tutorial, I will use a blog as an example, but this logic can apply to any page on a website. For example, I also use it with my notes on the website.

What we will finish with is the below URL structure:

1- <- Posts 19-24
2- <- Posts 13-18
3- <- Posts 7-12
4- <- Posts 1-6

Check it out:

Blog Page of


So, what do you need to have done already before working through this tutorial?

As long as you have a working blog with some posts on it you will be good to go. For the purposes of this tutorial, I won't be covering how to set up sourcing content into Gatsby. Or, how to create the individual blog posts using a template file. But, if you are interested in these tutorials please let me know.

As long as you currently have a single page that has all your posts on it, you'll be good to go. πŸ˜ƒ

If you are looking to set up your blog for the first time, LogRocket has a great tutorial here.


Right, with all that out the way, let's get down to actually writing some code.

As mentioned above we should already have our posts sourced into Gatsby in the gatsby-node.js. Following this, they should display as one long list.

If your individual post page generation is already set up you should have a hook into the createPages API. If not that's okay, here's a quick overview.


`createPages` is an API of Gatsby Node that allows you to create pages dynamically. This API runs after the sourcing and transformation of nodes and the creation of the GraphQL schema is complete. Read more about it here.

In the following examples of code, I use ES6 Modules in my gatsby-node file, these aren't enabled by default. You can follow a tutorial I did on Twitter to set these up.

To start setting up the `createPages` API and the generation of the paginated pages, we need to add in the below code.

1export async function createPages(params) {
2 await Promise.all([turnBlogPostsIntoPages(params)]);

This allows us to hook into the createPages API. The `Promise.all` isn't necessary, you could add all the code into this one function. But, for readability, I separated out the individual generations into their own functions.

To illustrate this point, here is a direct copy from my current website:

1export async function createPages(params) {
2 // After the creation of the nodes create pages for each custom type.
3 await Promise.all([
4 // Blog Posts
5 turnBlogPostsIntoPages(params),
6 // Notes Pages
7 turnNotesIntoPages(params),
8 // Turn sourced Twitter Threads into pages.
9 turnTwitterThreadsIntoPages(params),
10 // Blog Tags Pages
11 turnBlogPostTagsIntoPages(params),
12 // Turn Notes Categories into pages
13 turnNotesCategoriesIntoPages(params),
14 // Turn Twitter Threads Tags Into Pages
15 turnThreadsTagsIntoPages(params),
16 // Turn Portfolio Tags into pages
17 turnPortfolioTagsIntoPages(params),
18 // Turn Read Categories into pages.
19 turnReadsCategoriesIntoPages(params),
20 ]);

If I put all these functions into the `createPages` function, the function would be over 300 lines long. This definitely isn't good for our readability.

Anyway, back on topic. Next, we need to do create the `turnBlogPostsIntoPages` function that we declared.

So, anywhere above this `createPages` function add in a new function like so:

1async function turnBlogPostsIntoPages({ graphql, actions }) {
2 const { createPage } = actions;
5export async function createPages(params) {
6 await Promise.all([turnBlogPostsIntoPages(params)]);

Now, we have a function that gets called when the `createPages` API runs. Let's dive deeper into this function and start making it do something useful.

Here is the finished code which we will break down in a second.

1async function turnBlogPostsIntoPages({ graphql, actions }) {
2 // 0: Destructuring out the action createPage which is used to create the pages.
3 const { createPage } = actions;
5 // 1: Query for all of blog post data. Most importantly the totalCount which we will use later on.
6 const {
7 data: {
8 blog: { edges: blogPosts, totalCount: blogTotalCount },
9 },
10 } = await graphql(`
11 query {
12 blog: allMdx(filter: { fields: { contentCategory: { eq: "blog" } } }) {
13 edges {
14 node {
15 fields {
16 slug
17 contentCategory
18 }
19 frontmatter {
20 title
21 date(formatString: "DD/MM/YYYY")
22 }
23 }
24 }
25 totalCount
26 }
27 }
28 `);
30 // 2: Create a page for every blog post node. (This is for indivual blog posts.)
31 blogPosts.forEach(({ node }, index) => {
32 createPage({
33 path: node.fields.slug,
34 component: path.resolve("./src/templates/Blog.js"),
35 context: {
36 slug: node.fields.slug,
37 prev: index === 0 ? null : blogPosts[index - 1].node,
38 next: index === blogPosts.length - 1 ? null : blogPosts[index + 1].node,
39 },
40 });
41 });
43 // 3: Create the main blog pages containing the posts..
44 const blogTemplate = path.resolve("./src/pages/blog.js");
46 const pageSize = 6; // Total number of posts on each page
47 const pageCount = Math.ceil(blogTotalCount / pageSize); // Total number of pages required.
49 // Loop through each page required (1 to x) and create a new blog page for each.
50 Array.from({ length: pageCount }).forEach((_, i) => {
51 createPage({
52 path: `/blog/${i === 0 ? "" : i + 1}`,
53 blogTemplate,
54 // Context is passed to the page so we can skip the required amount of posts on each page.
55 context: {
56 skip: i * pageSize,
57 currentPage: i + 1,
58 pageSize,
59 },
60 });
61 });

If you have completed the setup of a blog on GatsbyJS you may already have some code that resembles this. Let's break down each step and see what's going on.

  1. This is where we destructure out the createPage action. This is used to actually create the pages from the information we provide.
  2. We query for all the information related to the posts sourced in GraphQL. Most importantly the `totalCount` number, as this is used to work out how many pages we need later on.
  3. This is where the individual blog posts are created using the createPage action we destructured above.
  4. This is the step we are interested in for this tutorial. This is where we create the paginated blog posts. Let's break this down in more detail below.

Breaking Down the Pagination

Here's the code we are particularly interested in for this tutorial:

1const blogTemplate = path.resolve("./src/pages/blog.js");
3const pageSize = 6; // Total number of posts on each page
4const pageCount = Math.ceil(blogTotalCount / pageSize); // Total number of pages required.
6// Loop through each page required (1 to x) and create a new blog page for each.
7Array.from({ length: pageCount }).forEach((_, i) => {
8 createPage({
9 path: `/blog/${i === 0 ? "" : i + 1}`,
10 blogTemplate,
11 // Context is passed to the page so we can skip the required amount of posts on each page.
12 context: {
13 skip: i * pageSize,
14 currentPage: i + 1,
15 pageSize,
16 },
17 });

Let's check out what's going on here.

1const blogTemplate = path.resolve("./src/pages/blog.js");

First, we import the template file which we will be using to create all the paginated pages. Note that this is the actual blog page in your pages directory. Not the file used for creating individual posts located within the templates folder.

1const pageSize = 6; // Total number of posts on each page
2const pageCount = Math.ceil(blogTotalCount / pageSize); // Total number of pages required.

Following this, we create two more variables that will control the page size and how many pages we need. This is achieved by setting the `pageSize` variable to the number of posts we want on each page. Then we use this variable to work out how many pages we need.

We achieve this by taking the `totalnumber` from our graphQL query and dividing it by the page size. By rounding up this result it tells us how many pages we need.

Then we get to the juicy part:

1// Loop through each page required (1 to x) and create a new blog page for each.
2Array.from({ length: pageCount }).forEach((_, i) => {
3 createPage({
4 path: `/blog/${i === 0 ? "" : i + 1}`,
5 blogTemplate,
6 // Context is passed to the page so we can skip the required amount of posts on each page.
7 context: {
8 skip: i * pageSize,
9 currentPage: i + 1,
10 pageSize,
11 },
12 });

What's going on here?

The most important part of this is what happens on line 2. This is the part that enables us to create multiple blog pages.

1Array.from({ length: pageCount }).forEach((_, i) => {});

We have a variable telling us how many pages we need (`pageCount`). But, for this to be of any use to us, we need to be able to loop through all the numbers leading to this point. This would allow us to create an individual page for each number.

This is where the above line comes in. We use `Array.from({ length: pageCount })` to create an array with the length of the number of pages we need to create. Yes, the array items will be blank but that doesn't matter because we are only interested in the indexes.

To get access to the indexes we chain onto the `Array.from()` with a `.forEach((_, i) => {})` like shown above. Because we are only interested in the indexes, we can skip the first argument to forEach. This would represent the actual item (blank) for the index we are currently on in the array. This then allows us access to the second argument which represents the index of the current item.

We are now able to loop through an array of variable length to allow us to create the required number of pages. Now we need to do the actual creation.

Which we can do with the following code:

2 path: `/blog/${i === 0 ? "" : i + 1}`,
3 component: blogTemplate,
4 // Context is passed to the page so we can skip the required amount of posts on each page.
5 context: {
6 skip: i * pageSize,
7 currentPage: i + 1,
8 pageSize,
9 },

Let's break down what's going on here.

We are passing a series of arguments to the `createPage` action, these are:

  • path: The URL the page will be created for.
  • component: This is the file that will be used to template out the page.
  • context: An object containing data passed to the page.
    • skip: We pass this to tell the page how many posts to skip in the GraphQL query. We work this out by multiplying the current index of the item we are on by the page size. So page 1 would be: `0 * 6 = 0` and for page 2 it would be: `1 * 6 = 6`. This ensures we skip the correct number of posts for each page.
    • currentPage: Tells the page what page it currently is which we will use for navigation later on.
    • pageSize: The size of the page we defined.

The `currentPage` context data will make more sense in the final section of this blog post. This is when we will look at creating pagination controls to move between the pages we have created. But, now let's look at the changes we need to make to the page file so it can consume the `skip` and `pageSize` context we passed.

Amending the Page File

Now, we have the `gatsby-node.js` file setup and ready to create all the pages for us. We now need to amend the page file that lists all the posts for us.

Updating the GraphQL Query

First, we need to update the GraphQL query that runs on the page. You should already have something that looks like this:

1export const query = graphql`
2 query {
3 blog: allMdx(
4 sort: {
5 order: [DESC, DESC]
6 fields: [frontmatter___date, frontmatter___id]
7 }
8 filter: { fields: { contentCategory: { eq: "blog" } } }
9 ) {
10 edges {
11 node {
12 fields {
13 slug
14 }
15 frontmatter {
16 date(formatString: "DD/MM/YYYY")
17 tags
18 title
19 id
20 image {
21 childImageSharp {
22 fluid(maxWidth: 400) {
23 ...GatsbyImageSharpFluid
24 }
25 }
26 }
27 }
28 }
29 }
30 totalCount
31 }
32 }

The important part to note in this query is that we are fetching all the blog posts. At this point, there are no constraints on the query other than a filter I've done to select all the blog content.

Now, let's look at what we need to add in.

Remember when we edited the `gatsby-node.js` file we passed a few pieces of context down to the page? Most notably `skip` and `pageSize`. Well because we passed them as context to the page we have them available to us to use in GraphQL as variables.

Let's take a look at the amended query and then we'll break them down:

1export const query = graphql`
2 query($skip: Int = 0, $pageSize: Int = 6) {
3 blog: allMdx(
4 limit: $pageSize
5 skip: $skip
6 sort: {
7 order: [DESC, DESC]
8 fields: [frontmatter___date, frontmatter___id]
9 }
10 filter: { fields: { contentCategory: { eq: "blog" } } }
11 ) {
12 edges {
13 node {
14 fields {
15 slug
16 }
17 frontmatter {
18 date(formatString: "DD/MM/YYYY")
19 tags
20 title
21 id
22 image {
23 childImageSharp {
24 fluid(maxWidth: 400) {
25 ...GatsbyImageSharpFluid
26 }
27 }
28 }
29 }
30 }
31 }
32 totalCount
33 }
34 }

As you can see in the top line of the query we added the two variables we passed as context. Let's look at this line in more depth:

1query($skip: Int = 0, $pageSize: Int = 6 ) {

If you're unfamiliar with GraphQL syntax. Below is a quick overview of what is happening.

  • $skip: This is the name of the variable
  • Int: This is the type of data the variable is
  • = 0: This is the default value of the variable.

So, in this line, we are taking in two variables which are both Integers (Ints) and giving them a default value.

Now, let's take a look at where they're used in the query:

1query($skip: Int = 0, $pageSize: Int = 6 ) {
2 blog: allMdx(
3 limit: $pageSize
4 skip: $skip
5 sort: { order: [DESC, DESC], fields: [frontmatter___date, frontmatter___id] }
6 filter: { fields: { contentCategory: { eq: "blog" } } }
7 )
8 // Rest of the query goes here...

As you can see we have added the `skip` and `pageSize` variables. They are passed to the `skip` and `limit` properties of the query.

If we were to translate this to the actual values of page 1, it would look like this:

1query($skip: Int = 0, $pageSize: Int = 6 ) {
2 blog: allMdx(
3 limit: 6
4 skip: 0
5 sort: { order: [DESC, DESC], fields: [frontmatter___date, frontmatter___id] }
6 filter: { fields: { contentCategory: { eq: "blog" } } }
7 )
8 // Rest of the query goes here...

This is because we want to limit the results to 6 values and we are skipping 0 because we are on the first page. If we were to look at page 2 as an example, we can start to see how it works.

1query($skip: Int = 0, $pageSize: Int = 6 ) {
2 blog: allMdx(
3 limit: 6
4 skip: 6
5 sort: { order: [DESC, DESC], fields: [frontmatter___date, frontmatter___id] }
6 filter: { fields: { contentCategory: { eq: "blog" } } }
7 )
8 // Rest of the query goes here...

The only thing that has changed is the skip value. Now, instead of skipping 0 values, we skip the first 6. Then we return a max of 6 values starting from item 7 in the dataset.

This is repeated for every page we generate until we have covered all the posts.

NOTE: If you want your first page to show the latest post you need to use the `sort` property in the query. See my example above for how to sort using the frontmatter___date field.

Pagination Controls

It's great having our posts paginated out over several pages. But, if the only way we have to move between them is by changing the URL we're going to have issues.

This is where adding pagination controls will help us. We're going to add a couple of dedicated buttons that allow next and previous movement on pages. We will also add a list of pages in the middle of the buttons so you can skip pages if you wish.

Creating the Pagination Component

Before we can implement this into our page we first need to create our `Pagination` component. Let's take a look at the code now.

1export default function Pagination({ pageSize, totalCount, currentPage }) {
2 const totalPages = Math.ceil(totalCount / pageSize);
3 const prevPage = currentPage - 1;
4 const nextPage = currentPage + 1;
5 const hasNextPage = nextPage <= totalPages;
6 const hasPrevPage = prevPage >= 1;
8 const prevLink = prevPage === 1 ? "blog" : `blog/${prevPage}`;
9 const nextLink = `blog/${nextPage}`;
11 return (
12 <div>
13 <Link title="prev page" disabled={!hasPrevPage} to={prevLink}>
14 &#8592; <span className="word">Previous</span>{" "}
15 </Link>
16 {Array.from({ length: totalPages }).map((_, i) => (
17 <Link
18 key={`blog-page-${i}`}
19 className={currentPage === 1 && i === 0 ? "current" : ""}
20 to={`blog/${i === 0 ? "" : i + 1}`}
21 >
22 {i + 1}
23 </Link>
24 ))}
25 <Link title="next page" disabled={!hasNextPage} to={nextLink}>
26 <span className="word">Next</span> &#8594;
27 </Link>
28 </div>
29 );

The code isn't too complex, we have a couple of `Link` elements from Gatsby. Then another one of our `Array.from()` specials from earlier. This time though instead of using a `.forEach()` we use a `.map()` because we want to return the elements.

Let's break this down.

The first section we are going to take a look at is the code that generates the links for us based on the props we pass in. Here's the code:

1export default function Pagination({
2 pageSize,
3 totalCount,
4 currentPage
5}) {
6 const totalPages = Math.ceil(totalCount / pageSize);
7 const prevPage = currentPage - 1;
8 const nextPage = currentPage + 1;
9 const hasNextPage = nextPage <= totalPages;
10 const hasPrevPage = prevPage >= 1;
12 const prevLink = prevPage === 1 ? "blog" : `blog/${prevPage}`;
13 const nextLink = `blog/${nextPage}`;
15 // ... Rest of the component here

So, what are we doing here?

First, we pass in a series of props:

  1. pageSize: The number of posts we want on each page.
  2. totalCount: The total amount of blog posts there are from the GraphQL query on the blog page
  3. currentPage: This comes from the context we added in `gatsby-node.js`. It is destructured out on the page and then passed down to the component.

After this, we create a series of variables that are used to determine if there is another page to navigate to. And, if so what the URL is.

  1. totalPages: Int: The total amount of pages required
  2. prevPage: Int: Works out the index of the previous page based on the current one.
  3. nextPage: Int: Works out the index of the next page based on the current one.
  4. hasNextPage: Bool: Checks if `nextPage` is smaller than or equal to `totalPages`.
  5. hasPrevPage: Bool: Checks if the `prevPage` variable is greater than or equal to one.

These 5 variables are used throughout the component to decide whether elements should be displayed or not. But, we will come to that in a second. First, we need to create the next and previous links which are our final two variables.

  1. prevLink: Str: Checks if the `prevPage` variable is equal to one. If so set the variable to `'blog'` which will redirect you to the main blog page. If not, set the variable to `blog/${prevPage}`.
  2. nextLink: Str: This is set to `blog/${nextPage}`. This is done because we have a separate variable controlling whether there is another page or not.

Now we've covered all the variables required to create the elements, let's look at making the buttons.

Next and Previous Buttons

The code for the previous and next buttons is actually similar with a few word changes. For this reason, I'm only going to break one of them down so we can understand how they both work. First let's look at the complete code so you can see the differences:

1<Link title="prev page" disabled={!hasPrevPage} to={prevLink}>
2 &#8592; <span className="word">Previous</span>{' '}
5<Link title="next page" disabled={!hasNextPage} to={nextLink}>
6 <span className="word">Next</span> &#8594;

The structure of the links is a Gatsby `Link` element wrapping a `span` describing what the button does. Let's check out what we do with them:

  1. title: Str: We set a title attribute on the element to declare to the user what each link does.
  2. disabled: Bool: This is where the two variables `hasPrevPage` and `hasNextPage` come in. If the variable says that the next or previous page is outside the available pages it will disable the link. This is then styled by CSS to lower the opacity and prevent the user from clicking on them. Check below for this code.
  3. to: Str: This is the destination we want the element to navigate to if clicked.

This covers the functionality, the last part is adding the text elements to label them up on the screen. For this, we use a `span` wrapping the relevant word and a next/previous arrow depending on the button.

CSS Code to disable the link:

1&[disabled] {
2 pointer-events: none;
3 text-decoration: line-through;
4 color: var(--black);
5 opacity: 0.5;
6 }

Adding All the Pages

We're almost there with the pagination component. We just need to add in the links for each page between the previous and next buttons.

Here's the code for adding in the page links:

2 Array.from({ length: totalPages }).map((_, i) => (
3 <Link
4 key={`blog-page-${i}`}
5 className={currentPage === 1 && i === 0 ? "current" : ""}
6 to={`blog/${i === 0 ? "" : i + 1}`}
7 >
8 {i + 1}
9 </Link>
10 ));

It's based on a similar principle to what we used in the past for generating each page in `gatsby-node.js`. However, instead of using a `.forEach()` to loop over each index, we use a `.map()` so we can return the `Link` element.

Let's break down what's happening in this code block:

  1. We create an array with the length equal to `totalPages` and then map over each index.
  2. We create a `Link` element and pass 3 props to it:
  3. key: Str: Standard key prop required by React when returning elements from a map.
  4. className: Str: Used to add the 'current' class to the element if we are on that page to allow for styling.
  5. to: Str: The destination we want the link to navigate to.
  6. Finally, we add the text inside the `Link` which is the index plus 1 to negate the 0-indexing of JavaScript arrays.

Here is the CSS for the current element styling we added:

2&.current {
3 color: var(--green);
4 font-weight: 600;
5 border-bottom: 2px solid var(--green);

Adding to the Page File

We have now created the pagination component, it's time to add it to our blog page file like so:

1return (
2 <>
3 <Pagination
4 pageSize={6}
5 totalCount={totalCount}
6 currentPage={currentPage || 1}
7 />
8 <AllPostsContainer>
9 { => (
10 <BlogPostCard
11 key={`blogPostCard-${}`}
12 post={post}
13 />
14 ))}
15 </AllPostsContainer>
16 <Pagination
17 pageSize={6}
18 totalCount={totalCount}
19 currentPage={currentPage || 1}
20 />
21 </>

As we have split the entire pagination component, it's now a matter of passing in the required props:

  1. pageSize: Int: The total number of posts on each page
  2. totalCount: Int: The total number of posts from the GraphQL query.
  3. currentPage: Int: The index of the current page taken from context, if not 1.

We should now be set up and ready to rock and roll.

Summing Up

If everything went to plan you should have something that resembles this for functionality:

Blog Page of

I didn't include the CSS in this article apart from one or two sections to try and keep the length of the post down. But, if you're interested in all the CSS it is available on the Github for my website.

One thing I didn't include in this post but do recommend is storing your `pageSize` variable in a `.env` file. Then you can import it into Gatsby using `parseInt(process.env.GATSBY_PAGE_SIZE)}`. This means that should you ever want to change the number of posts per page you only need to change it in one place.

I hope you found this post helpful if you did I would greatly appreciate you sharing it with others on Social Media so they can benefit from it too. If you have any feedback I would love to hear it, you can reach me over on Twitter.

Thanks for reading. πŸ˜ƒ


Latest Blog Posts

Below is my latest blog post and a link to all of my posts.

View All Posts


Latest Video

Here is my YouTube Channel and latest video for your enjoyment.

View All Videos
AWS Bedrock Text and Image Generation Tutorial (AWS SDK)

AWS Bedrock Text and Image Generation Tutorial (AWS SDK)


Join My Newsletter

Subscribe to my weekly newsletter by filling in the form.

Get my latest content every week and 0 spam!