Prismic cursor-based GraphQL pagination in Gatsby

2020-03-31

I use Prismic as a headless CMS to host all my content (including images) and fetch all of that data using a plugin in Gatsby. One of the last steps when finishing my blog was adding pagination, for some reason I had a bit of trouble getting it to work. There isn't a lot of information available online so let me share my findings so you might have an easier time. This is an update!

1. Creating pages - gatsby-node.js

Inside this file you have the opportunity to create pages and that's what I did:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
exports.createPages = async ({ actions, graphql, reporter }) => {
  const { createPage, createRedirect } = actions

  const result = await graphql(`
    {
      prismic {
        allPosts {
          totalCount
        }
      }
    }
  `)

  const totalCount = result.data.prismic.allPosts.totalCount
  const postsPerPage = 4
  const numPages = Math.ceil(totalCount / postsPerPage)

  reporter.info(`generating ${numPages} blog listing page(s)`)

  Array.from({ length: numPages }).forEach((_, i) => {
    const currentPage = i + 1
    const pageProperties = {
      path: i === 0 ? `/` : `/page/${i + 1}/`,
      component: path.resolve("./src/templates/BlogListTemplate.tsx"),
      context: {
        limit: postsPerPage,
        numPages,
        currentPage,
      },
    }

    if (i !== 0) {
      pageProperties.context.cursor = btoa(
        `arrayconnection:${i * postsPerPage - 1}`
      )
    }

    createPage(pageProperties)
  })
}

Let me explain what is happening in the code above:

  1. Fetch the totalCount of posts using a GraphQL query
  2. Calculate the number of pages Math.ceil(totalCount / postsPerPage), keeping in mind the desired amount of post per page.
  3. Build a pageProperties object. This object contains context that will be shared with the template (BlogListTemplate.tsx)
  4. Creates a page using the pageProperties object

You might also have noticed the cursor property that is set in the context property. This value will be used to actually fetch the correct posts in the template, but more on the later. NOTE: an extra devDependency is required to make this work

1
yarn add -D btoa

In general, we've found that cursor-based pagination is the most powerful of those designed. Especially if the cursors are opaque, either offset or ID-based pagination can be implemented using cursor-based pagination (by making the cursor the offset or the ID), and using cursors gives additional flexibility if the pagination model changes in the future. As a reminder that the cursors are opaque and that their format should not be relied upon, we suggest base64 encoding them.

- https://graphql.org/learn/pagination/#pagination-and-edges

2. Creating a template - BlogListTemplate.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export const query = graphql`
  query BlogList($limit: Int!, $cursor: String) {
    prismic {
      allPosts(sortBy: date_DESC, first: $limit, after: $cursor) {
        edges {
          node {
            _meta {
              id
              uid
            }
            title
            date
            image
            excerpt
          }
        }
      }
    }
  }
`

The query receives the $limit and $cursor parameter from the context passed by the createPage() method from gatsby-node.js.
By using allPosts(sortBy: date_DESC, first: $limit, after: $cursor) we make sure only 4 post (in our case) are returned by the GraphQL API.
The $cursor is equal to the cursor of the last document of the previous page.

3. Display page information

1
2
3
4
5
6
7
8
9
10
11
12
const BlogList: React.FC<Props> = ({ data, pageContext }: Props) => {
  const posts = data.prismic.allPosts.edges

  return (
    <LayoutComponent>
        <BlogPosts posts={posts} />
        <PageNavigation pageContext={pageContext} />
    </LayoutComponent>
  )
}

export default BlogList

Our component receives the post data but also the page context as props.

1
2
3
4
5
export interface PageContext {
  limit: number
  numPages: number
  currentPage: number
}

You can write your own PageNavigation component as I did and use the context to show and/or hide the previous and next button, show page numbers,...

If you have any questions, do not hesitate to contact me or leave a comment below.

Created by Jeroen Druwé