JavaScript Development Space

Create Reusable Pagination System in Gatsby with GraphQL

Add to your RSS feed16 September 20249 min read
Create Reusable Pagination System in Gatsby with GraphQL

Creating a reusable pagination system in Gatsby using GraphQL without relying on external plugins involves leveraging Gatsby's built-in createPages API and writing reusable pagination logic. Here’s a step-by-step guide to building a simple yet scalable pagination system that can be reused across multiple content types such as posts, categories, and tags.

1. Setup Your Gatsby Website

Before creating a Gatsby website, you need to install the Gatsby CLI (Command Line Interface) globally on your machine.

Install Gatsby CLI

npm install -g gatsby-cli

This command installs the Gatsby CLI globally on your machine so that you can run Gatsby commands from anywhere.

Create a New Gatsby Project

After installing the CLI, you can create a new Gatsby project using one of Gatsby’s starter templates.

Run the following command in your terminal to create a new project:

gatsby new

Check for the tailwind and MDX

bash
1 What would you like to call your site?
2 √ · gatsby-pagionation
3 What would you like to name the folder where your site will be created?
4 √ NextJS/ gatsby-pagionation
5 √ Will you be using JavaScript or TypeScript?
6 · TypeScript
7 √ Will you be using a CMS?
8 · No (or I'll add it later)
9 √ Would you like to install a styling system?
10 · Tailwind CSS
11 √ Would you like to install additional features with other plugins?
12
13
14 Thanks! Here's what we'll now do:
15
16 Create a new Gatsby site in the folder gatsby-pagionation
17 Get you set up to use Tailwind CSS for styling your site
18 Install gatsby-plugin-mdx
19
20
21 √ Shall we do this? (Y/n) · Yes

This will create a new Gatsby site in a directory called gatsby-pagination.

cd gatsby-pagionation

2. Add MDX content

Add some mdx content to your content directory.

3. Setup ShadcnUI

Update the tsconfig.json file to integrate with Shadcn. Open tsconfig.json and configure it as follows:

json
1 {
2 "compilerOptions": {
3 // ...
4 "baseUrl": ".",
5 "paths": {
6 "@/*": ["./src/*"]
7 }
8 // ...
9 }
10 }

Update/create gatsby-node.ts file

js
1 import * as path from 'path';
2
3 export const onCreateWebpackConfig = ({ actions }) => {
4 actions.setWebpackConfig({
5 resolve: {
6 alias: {
7 '@/components': path.resolve(__dirname, 'src/components'),
8 '@/lib/utils': path.resolve(__dirname, 'src/lib/utils'),
9 },
10 },
11 });
12 };

Run the Shadcn CLI

npx shadcn@latest init

Answer the questions

bash
1 ✔ Preflight checks.
2 ✔ Verifying framework. Found Gatsby.
3 ✔ Validating Tailwind CSS.
4 ✔ Validating import alias.
5 √ Which style would you like to use? » New York
6 √ Which color would you like to use as the base color? » Neutral
7 √ Would you like to use CSS variables for theming? ... no / yes
8 ✔ Writing components.json.
9 ✔ Checking registry.
10 ✔ Updating tailwind.config.js
11 ✔ Updating src\styles\global.css
12 ✔ Installing dependencies.
13 ✔ Created 1 file:
14 - src\lib\utils.ts
15
16 Success! Project initialization completed.
17 You may now add components.

Enable gatsby-source-filesystem plugin

Modify you gatsby-config.ts:

js
1 {
2 resolve: `gatsby-source-filesystem`,
3 options: {
4 name: 'posts',
5 path: `./content`,
6 },
7 },

Create a post template

Inside templates folder create a new file, called post.tsx

js
1 import * as React from 'react';
2 import { graphql, HeadFC, PageProps } from 'gatsby';
3 import { MDXProvider } from '@mdx-js/react';
4
5 const PostTemplate: React.FC<PageProps<Queries.GetSinglePostQuery>> = ({
6 data: {
7 mdx: {
8 frontmatter: { title },
9 },
10 },
11 children,
12 }) => {
13 return (
14 <div className='my-16 px-64'>
15 <article className='text-lg'>
16 <h1 className='tracking-wide text-4xl font-medium space-y-5 my-5'>{title}</h1>
17 <MDXProvider> {children}</MDXProvider>
18 </article>
19 </div>
20 );
21 };
22 export const query = graphql`
23 query GetSinglePost($slug: String) {
24 mdx(frontmatter: { slug: { eq: $slug } }) {
25 frontmatter {
26 title
27 }
28 }
29 }
30 `;
31
32 export const Head: HeadFC<Queries.GetSinglePost, unknown> = ({
33 data: {
34 mdx: {
35 frontmatter: { title },
36 },
37 },
38 }) => {
39 return (
40 <>
41 <title>{title}</title>
42 </>
43 );
44 };
45
46 export default PostTemplate;

Create a page template

js
1 // page.template.tsx
2
3 import React from 'react';
4 import { graphql, HeadFC, PageProps, useStaticQuery } from 'gatsby';
5
6 const PageTemplate: React.FC<PageProps<Queries.GetAllPostsQuery>> = ({
7 data: {
8 allMdx: { nodes: posts },
9 },
10 pageContext: { currentPage, numPages, slug = '' },
11 }) => {
12 return (
13 <section className='category mt-[60px] mb-[60px] flex flex-col gap-20 items-center'>
14 <h1 className='tracking-wide text-4xl font-bold space-y-5 my-5 font-ptSerif'>
15 {`Page ${currentPage}`}
16 </h1>
17 {posts.map((post) => {
18 return (
19 <div key={post.id}>
20 <h2>{post.frontmatter.title}</h2>
21 </div>
22 );
23 })}
24 </section>
25 );
26 };
27
28 export const query = graphql`
29 query GetAllPosts($skip: Int!, $limit: Int!) {
30 allMdx(limit: $limit, skip: $skip) {
31 nodes {
32 frontmatter {
33 slug
34 title
35 }
36 id
37 }
38 }
39 }
40 `;
41
42 export default PageTemplate;

Enable MDX plugin

In gatsby-config.ts add properties to 'gatsby-plugin-mdx'

js
1 {
2 resolve: 'gatsby-plugin-mdx',
3 options: {
4 extensions: ['.md', '.mdx'],
5 },
6 },

Set Up GraphQL Queries

Before implementing pagination, ensure that your data sources are set up and that you can query posts, categories, and tags through GraphQL.

js
1 // gatsby-config.mjs
2
3 import * as path from 'path';
4
5 const POSTS_PER_PAGE = 1;
6
7 const getNumPages = (count) => {
8 return Math.ceil(count / POSTS_PER_PAGE) || 1;
9 };
10
11 export const onCreateWebpackConfig = ({ actions }) => {
12 actions.setWebpackConfig({
13 resolve: {
14 alias: {
15 '@/components': path.resolve('./', 'src/components'),
16 '@/lib/utils': path.resolve('./', 'src/lib/utils'),
17 },
18 },
19 });
20 };
21
22 export const createPages = async ({ graphql, actions, reporter }) => {
23 const { createPage } = actions;
24 const postTemplate = path.resolve(`src/templates/post.template.tsx`);
25 const pageTemplate = path.resolve(`src/templates/page.template.tsx`);
26
27 const result = await graphql(
28 `
29 query GatsbyNodeCreatePages {
30 allMdx {
31 nodes {
32 frontmatter {
33 slug
34 }
35 internal {
36 contentFilePath
37 }
38 }
39 }
40 }
41 `,
42 );
43
44 if (result.errors) {
45 reporter.panicOnBuild('Error loading MDX result', result.errors);
46 }
47
48 const posts = result.data.allMdx.nodes;
49
50 posts.forEach((node) => {
51 createPage({
52 path: `/${node.frontmatter.slug}`,
53 component: `${postTemplate}?__contentFilePath=${node.internal.contentFilePath}`,
54 context: {
55 slug: node.frontmatter.slug,
56 },
57 });
58 });
59
60 const numPages = getNumPages(posts.length);
61 if (numPages > 1) {
62 Array.from({ length: numPages }).forEach((_, i) => {
63 if (i !== 0) {
64 createPage({
65 path: `/${i + 1}`,
66 component: pageTemplate,
67 context: {
68 limit: POSTS_PER_PAGE,
69 skip: i * POSTS_PER_PAGE,
70 numPages,
71 currentPage: i + 1,
72 },
73 });
74 }
75 });
76 }
77 };

It's time to test

npm run develop

If everything is working, then let's add small helper function - getNumPages() to our lib/utils.ts file:

js
1 export const POSTS_PER_PAGE = 1;
2
3 export const getNumPages = (count: number): number => {
4 return Math.ceil(count / POSTS_PER_PAGE) || 1;
5 };

Create a index page

Let's fetch all our posts to index.ts file:

js
1 import React from 'react';
2 import { graphql, type HeadFC, type PageProps } from 'gatsby';
3
4 import { getNumPages } from '../lib/utils';
5
6 const IndexPage: React.FC<PageProps<Queries.IndexPageQuery>> = ({
7 data: {
8 allMdx: { nodes: posts, totalCount },
9 },
10 }) => {
11 const numPages = getNumPages(totalCount);
12
13 return (
14 <main className='w-full h-full flex items-center flex-col gap-20'>
15 <h1 className='text-2xl md:text-3xl my-5 font-ptSerif'>Gatsby Pagination</h1>
16 <p className='w-10/12 text-xl'>Pagination in GatsbyJS</p>
17 {posts.map((post, idx) => {
18 return (
19 <div key={post.id}>
20 <h2>{post.frontmatter.title}</h2>
21 </div>
22 );
23 })}
24 </main>
25 );
26 };
27
28 export default IndexPage;
29
30 export const query = graphql`
31 query IndexPage {
32 allMdx {
33 nodes {
34 frontmatter {
35 slug
36 title
37 }
38 id
39 }
40 totalCount
41 }
42 }
43 `;

Then run

npm run clean && npm run develop

The result

Gatsby setup result

Navigate to http://localhost:8000/2 to check the pages.

2. Create a Pagination Component

Before creating the pagination component, you need to setup Pagination component from ShadcnUI.

npx shadcn@latest add pagination

Last one thing you need to integrate pagination component to Gatsby.*:

Find PaginationLinkProps and PaginationLink inside ui/pagination.tsx, and replace it with:

js
1 type PaginationLinkProps = {
2 isActive?: boolean,
3 to: string,
4 } & Pick<ButtonProps, 'size'> &
5 React.ComponentProps<'a'>;
6
7 const PaginationLink = ({
8 className,
9 isActive,
10 to,
11 size = 'icon',
12 ...props
13 }: PaginationLinkProps) => (
14 <Link
15 to={`/${to}`}
16 aria-current={isActive ? 'page' : undefined}
17 className={cn(
18 buttonVariants({
19 variant: isActive ? 'outline' : 'ghost',
20 size,
21 }),
22 className,
23 )}
24 {...props}
25 />
26 );

Don't forget to import the Link from Gatsby.

Now create file posts-pagination.tsx inside your components folder.

js
1 import React from 'react';
2
3 import {
4 Pagination,
5 PaginationContent,
6 PaginationEllipsis,
7 PaginationItem,
8 PaginationLink,
9 PaginationNext,
10 PaginationPrevious,
11 } from './ui/pagination';
12
13 type PostsPaginationProps = {
14 numPages: number,
15 currentPage: number,
16 slug?: string,
17 };
18
19 const PostsPagination = ({ numPages, currentPage, slug = '' }: PostsPaginationProps) => {
20 return (
21 <Pagination className='mt-10'>
22 <PaginationContent>
23 {currentPage !== 1 && (
24 <PaginationItem>
25 <PaginationPrevious to={`${slug}${currentPage - 1 === 1 ? '' : currentPage - 1}`} />
26 </PaginationItem>
27 )}
28 {currentPage - 2 > 1 && (
29 <PaginationItem>
30 <PaginationEllipsis />
31 </PaginationItem>
32 )}
33 {Array.from({ length: numPages }).map((_, i) => {
34 const link = slug + '' + (i === 0 ? '' : i + 1);
35 if (i > currentPage + 1 || i < currentPage - 3) {
36 return;
37 }
38 return (
39 <PaginationItem key={i}>
40 <PaginationLink isActive={i + 1 === currentPage} to={link}>
41 {i + 1}
42 </PaginationLink>
43 </PaginationItem>
44 );
45 })}
46 {numPages > 3 && currentPage + 2 < numPages && (
47 <PaginationItem>
48 <PaginationEllipsis />
49 </PaginationItem>
50 )}
51 {currentPage !== numPages && (
52 <PaginationItem>
53 <PaginationNext to={slug + (currentPage + 1)} />
54 </PaginationItem>
55 )}
56 </PaginationContent>
57 </Pagination>
58 );
59 };
60 export default PostsPagination;

Let's break down each part of the code:

1. Imports

js
1 import React from 'react';
2 import {
3 Pagination,
4 PaginationContent,
5 PaginationEllipsis,
6 PaginationItem,
7 PaginationLink,
8 PaginationNext,
9 PaginationPrevious,
10 } from './ui/pagination';

Several UI components from the Shadcn Pagination Component are imported.

2. Component Props

js
1 type PostsPaginationProps = {
2 numPages: number,
3 currentPage: number,
4 slug?: string,
5 };
  • numPages: Total number of pages to paginate through.
  • currentPage: The current page the user is on.
  • slug: An optional string that is appended to the URL for each page link. It defaults to an empty string if not provided. You can provide a string "tag/some-tag" or "category/some-category" to specify the path

3. Ellipsis Before and After Current Page

js
1 {
2 currentPage - 2 > 1 && (
3 <PaginationItem>
4 <PaginationEllipsis />
5 </PaginationItem>
6 );
7 }

If there are more than two pages between the first page and the current page (i.e., there are skipped pages), it shows an ellipsis (...).

js
1 {
2 numPages > 3 && currentPage + 2 < numPages && (
3 <PaginationItem>
4 <PaginationEllipsis />
5 </PaginationItem>
6 );
7 }
js
1 {
2 Array.from({ length: numPages }).map((_, i) => {
3 const link = slug + '' + (i === 0 ? '' : i + 1);
4 if (i > currentPage + 1 || i < currentPage - 3) {
5 return;
6 }
7 return (
8 <PaginationItem key={i}>
9 <PaginationLink isActive={i + 1 === currentPage} to={link}>
10 {i + 1}
11 </PaginationLink>
12 </PaginationItem>
13 );
14 });
15 }

This dynamically generates pagination links for each page.

This PostsPagination component is a reusable, well-structured pagination system for Gatsby or any React project. It efficiently handles pagination UI, showing a limited range of pages around the current one and allowing navigation via next/previous buttons. The ellipsis keeps the UI clean when dealing with a large number of pages.

Connect Pagination to Index and Page Template

js
1 const numPages = getNumPages(totalCount);
2 {
3 numPages > 1 && <PostsPagination currentPage={1} numPages={numPages} />;
4 }
npm run develop Final result

Now you've created a reusable pagination component in Gatsby. You can use this component to paginate through posts, categories, or tags. This scalable solution ensures that your content is well-organized and easy to navigate, especially as your dataset grows.

JavaScript Development Space

© 2024 JavaScript Development Space - Master JS and NodeJS. All rights reserved.