Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 26, 2021 09:38 am GMT

Paginating your Contentful blog posts in Next.js with the GraphQL API

In this post, were going to build a set of article list pages that display a number of blog post summaries per page fetched from the Contentful GraphQL API at build time. Well also include navigation to next and previous pages. Whats great about this approach is that it requires no client-side state. All article-list pages are pre-rendered into static HTML at build time. This requires a lot less code than you might think!

The benefits of Static Site Generation

Next.js is a powerful framework that offers Static Site Generation (SSG) for React applications. Static Site Generation is where your website pages are pre-rendered as static files using data fetched at build time (on the server), rather than running JavaScript to build the page in the browser (on the client) or on the server at the time someone visits your website (run time).

Some of the benefits of SSG:

  • Speed. Entire pages are loaded at first request rather than having to wait for client-side requests to fetch the data required.-Accessibility. Pages load without JavaScript.
  • Convenience. Host the files on your static hoster of choice (Netlify, Vercel or even good old GitHub pages) and call it a day!
  • Its scalable, fast and secure.

Heres how it looks in a complete Next.js starter. Click here to view a live demo of the code referenced in this post.

Screenshot of the blog index page on the Next.js Contentful blog starter example website

To build the article list pagination, were going to harness the power of Static Site Generation provided by Next.js via the following two asynchronous functions:

  • getStaticProps: Fetch data at build time
  • getStaticPaths: Specify dynamic routes to pre-render pages based on data

If youre new to Next.js, check out the documentation on Static Generation here.

Getting set up

I created a Next.js + Contentful blog starter repository that contains the completed code for statically generated article list pages described in this post. If youd like to explore the code before getting started with the tutorial, you can fork the repository on GitHub here.

Were going to create a fresh Next.js application and build up the functionality to understand how it all fits together.

For the purposes of this tutorial, you dont need a Contentful account or any of your own blog posts. Were going to connect to an example Contentful space that contains all of the data we need to build the article list pages and pagination. That being said, if you have an existing Contentful account and blog posts, you can connect your new Next.js application to your Contentful space with your own space ID and Contentful Delivery API access token. Just be sure to use the correct content type fields in your GraphQL queries if they are different to the example.

To spin up a new Next.js application, run the following command in your terminal:

npx create-next-app nextjs-contentful-pagination-tutorial

This command creates a new directory that includes all code to get started. This is what you should see after you run the command in your terminal window. (Ive truncated the output a little with ... but what youre looking for is Done!)

Screenshot of terminal output after running npx create-next-app

Navigate to the root of your project directory to view the files created for you.

cd nextjs-contentful-pagination-tutorialls -la

If this is what you see, youre good to go!

Screenshot of running ls -la after npx create-next-app

You now have a fresh Next.js application with all dependencies installed. But what data are we going to use to build the article list pages?

Retrieving example data

I created an example Contentful space that provides the data for the Next.js Contentful Blog Starter. It contains the content model we need and three blog posts so we can build out the pagination.

In the root of your project directory, create an .env.local file.

touch .env.local

Copy and paste the following into the .env.local file:

CONTENTFUL_SPACE_ID=84zl5qdw0oreCONTENTFUL_ACCESS_TOKEN=_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA

These credentials will connect the application to the example Contentful space to provide you with some data to build out the functionality.

We will use the following fields on the blogPost content type in our GraphQL queries to build the paginated article list:

  • Date (date & time)
  • Title (short text)
  • Slug (short text)
  • Tags (short text, list)
  • Excerpt (long text, presented in a markdown editor)

Screenshot of the basic content model described above in the Contentful Web App

Youre good to go if you have:

  • a fresh Next.js application
  • an .env.local file with the example credentials provided above

To run the application, navigate to the root of your project directory and run:

npm run dev

Youll need to stop and start your development server each time you add a new file to the application.

So, weve got a Next.js application and credentials that we can use to connect us to a Contentful space! What files do we need in our application to implement a paginated blog?

Building the routes

Were going to pre-render the following routes at build time, which will call out to the Contentful GraphQL API to get the data for each article list page:

  • /blog
  • /blog/page/2
  • /blog/page/3
  • etc

In the pages directory, create a new directory and name it blog. Add a file called index.js this will be the /blog route.

cd my-blog/pagesmkdir blog cd blogtouch index.js

Next, inside the blog directory, create a new directory and name it page. Create a new file inside that directory, and name it [page].js this will be our dynamic route, which will build the routes /blog/page/{pageNumber}. Read more about dynamic routes on the Next.js docs.

cd my-blog/pages/blogmkdir pagecd pagetouch [page].js

Heres what your file and folder structure should look like:

Screenshot in VS Code of the directories and files created above

Thats all it takes to set up the routes /blog/ and /blog/page/{pageNumber}, but theyre not doing anything yet. Lets get some data from Contentful.

Setting up the calls to the Contentful GraphQL API

To fill the pages with data, we need to make API calls. I prefer to define API calls in a dedicated file, so that they can be reused easily across the application. In this example, I created a ContentfulApi.js class, which can be found in the utils directory of the starter repository. We need to make two requests to the API to build our article list pages.

Create a utils directory at the root of your project, and create a new file named ContentfulApi.js.

Screenshot of the ContentfulApi.js file in the utils directory in VSCode

Before we start building the needed GraphQL queries, lets set up an asynchronous call to the Contentful GraphQL API that takes in a string parameter named query. Well use this twice later to request data from Contentful.

If youd like to learn more about GraphQL, check out Stefan Judiss free GraphQL course on YouTube.

To explore the GraphQL queries in this post using the Contentful GraphiQL playground, navigate to the following URL and paste any of the queries below into the explorer (without the const and =). The space ID and access token in the URL will connect you to the same Contentful space that you connected to via the .env.local file.

https://graphql.contentful.com/content/v1/spaces/84zl5qdw0ore/explore?access_token=_9I7fuuLbV9FUV1p596lpDGkfLs9icTP2DZA5KUbFjA

Add the following code to /utils/ContentfulApi.js.

// /utils/ContentfulApi.jsexport default class ContentfulApi {  static async callContentful(query) {    const fetchUrl = `https://graphql.contentful.com/content/v1/spaces/${process.env.CONTENTFUL_SPACE_ID}`;    const fetchOptions = {      method: "POST",      headers: {        Authorization: `Bearer ${process.env.CONTENTFUL_ACCESS_TOKEN}`,        "Content-Type": "application/json",      },      body: JSON.stringify({ query }),    };    try {      const data = await fetch(fetchUrl, fetchOptions).then((response) =>        response.json(),      );      return data;    } catch (error) {      throw new Error("Could not fetch data from Contentful!");    }  }}

Weve got our API call set up! Now, lets fetch some data.

Querying the total number of posts

In order to calculate how many dynamic page routes we need to build and statically generate on /blog/page/[page].js, we need to work out how many blog posts we have, and divide that by the number of posts we want to show on each page.

numberOfPages = totalNumberOfPosts / howManyPostsToDisplayOnEachPage

For this, its useful to define how many posts you want to show on each page in a global variable or configuration object. Well need to use it in a few different places.

For that reason, the Next.js + Contentful blog starter contains a Config.js file in the utils directory. Well use the exported Config object in our API calls.

Feel free to skip this step and use a hard-coded number if youre just exploring.

// /utils/Config.jsexport const Config = {  //...  pagination: {    pageSize: 2,  },};

In the same ContentfulApi class, lets create a new asynchronous method that will query and return the total blog number of blog posts.

// /utils/ContentfulApi.jsexport default class ContentfulApi {  static async callContentful(query) { /* GQL call described above */ }  static async getTotalPostsNumber() {    // Build the query    const query = `      {        blogPostCollection {          total        }      }    `;    // Call out to the API    const response = await this.callContentful(query);    const totalPosts = response.data.blogPostCollection.total      ? response.data.blogPostCollection.total      : 0;    return totalPosts;  }}

Weve successfully retrieved our total number of blog posts. Whats next?

Querying the post summaries by page number

Lets create a final asynchronous method that requests the number of blog post summaries we defined in Config.pagination.pageSize, by page number.

We also request the total number of blog posts in this query. Well need this later, and it saves us having to make two API calls when generating the /blog route.

Heres the code.

// /utils/ContentfulApi.jsexport default class ContentfulApi {  static async callContentful(query) { /* GQL call described above */ }  static async getTotalPostsNumber() { /* method described above */ }  static async getPaginatedPostSummaries(page) {    const skipMultiplier = page === 1 ? 0 : page - 1;    const skip =      skipMultiplier > 0 ? Config.pagination.pageSize * skipMultiplier : 0;    const query = `{        blogPostCollection(limit: ${Config.pagination.pageSize}, skip: ${skip}, order: date_DESC) {          total          items {            sys {              id            }            date            title            slug            excerpt            tags          }        }      }`;    // Call out to the API    const response = await this.callContentful(query);    const paginatedPostSummaries = response.data.blogPostCollection      ? response.data.blogPostCollection      : { total: 0, items: [] };    return paginatedPostSummaries;  } }

Observe that we are querying for the five fields referenced at the top of this post: date, title, slug, tags and excerpt plus the sys.id. This will be useful when rendering our data to the DOM.

The skip parameter in the GraphQL query is what does all the magic for us here. We calculate the skip parameter for the query based on the incoming page number parameter. For example, if we want to fetch the posts for page two, the skip parameter would be calculated as 1 x Config.pagination.pageSize, therefore skipping the results of page one.

If we want to fetch the posts for page six, the skip parameter would be calculated as 5 x Config.pagination.pageSize, and so on. When all your code is set up in your application, play around with the Config.pagination.pageSize to see this magic in action.

Weve now set up all the API calls we need to get our data to pre-render our blog page routes at build time. Lets fetch our data for page one on /blog.

Building the blog index with getStaticProps

The blog index will be available on /blog and will serve page one of our blog post summaries. For this reason, we can safely hardcode the number 1 in this file. This is great for readability think self-documenting code!

Lets pre-render this page at build time by exporting an async function called getStaticProps. Read more about getStaticProps on the Next.js documentation.

Add the following code to pages/blog/index.js.

// /pages/blog/index.jsimport ContentfulApi from "@utils/ContentfulApi";import { Config } from "@utils/Config";export default function BlogIndex(props) {  const { postSummaries, currentPage, totalPages } = props;  return (    // Well build the post list component later  );}export async function getStaticProps() {  const postSummaries = await ContentfulApi.getPaginatedPostSummaries(1);  const totalPages = Math.ceil(postSummaries.total / Config.pagination.pageSize);  return {    props: {      postSummaries: postSummaries.items,      totalPages,      currentPage: "1",    },  };}

Were using getStaticProps() to:

  • Request the post summaries for page one and total number of posts from the API.
  • Calculate the total number of pages based on the number of posts and Config.pagination.pageSize.
  • Return the postSummaries.items, totalPages, and currentPage as props to the BlogIndex component.

Bonus content!

Youll notice that the file imports from the utils directory in this example are imported using absolute paths via a module alias using @. This is a really neat way to avoid long relative path imports (../../../../..) in your Next.js application, which increases code readability.

You can define module aliases in a jsconfig.json file at the root of your project. Heres the jsconfig.json file used in the Next.js Contentful blog starter:

// jsconfig.json{  "compilerOptions": {    "baseUrl": "./",    "paths": {      "@components/*": ["components/*"],      "@utils/*": ["utils/*"]    }  }}

Read more on the official documentation.

Were going to be creating a components directory later on in this post, so I recommend adding this jsconfig.json file to your project to make file imports super easy. Make sure you stop and start your development server after adding this new file to enable Next.js to pick up the changes.

So thats fetching data for page one done! But how do we build the dynamic routes at build time based on how many blog posts we have, and how many posts we want to show per page?

Building the dynamic article list pages with getStaticPaths

The article list pages will be available on /blog/page/{pageNumber} starting with the second page (/blog/ is page one). This is where we need to use getStaticPaths() to define a list of paths that will be rendered to HTML at build time.The rendered paths are based on the total number of blog posts, and how many posts we want to show per page.

Lets tell Next.js which paths we want to statically render by exporting an async function called getStaticPaths. Read more about getStaticPaths on the Next.js documentation.

Add the following code to pages/blog/page/[page].js:

// /pages/blog/pages/[page].jsimport ContentfulApi from "@utils/ContentfulApi";import { Config } from "@utils/Config";export default function BlogIndexPage(props) {  const { postSummaries, totalPages, currentPage } = props;  return (    // Well build the post list component later  );}export async function getStaticPaths() {  const totalPosts = await ContentfulApi.getTotalPostsNumber();  const totalPages = Math.ceil(totalPosts / Config.pagination.pageSize);  const paths = [];  /**   * Start from page 2, so we don't replicate /blog   * which is page 1   */  for (let page = 2; page <= totalPages; page++) {    paths.push({ params: { page: page.toString() } });  }  return {    paths,    fallback: false,  };}export async function getStaticProps({ params }) {  const postSummaries = await ContentfulApi.getPaginatedPostSummaries(    params.page,  );  const totalPages = Math.ceil(postSummaries.total / Config.pagination.pageSize);  return {    props: {      postSummaries: postSummaries.items,      totalPages,      currentPage: params.page,    },  };

Were using getStaticPaths() to:

  • Request the total number of posts from the Contentful API.
  • Calculate the total number of pages we need to build depending on the page size we defined.
  • Build a paths array that starts from page two (blog/page/2) and ends at the total number of pages we calculated.
  • Return the paths array to getStaticProps so that for each path, Next.js will request the data for the dynamic page number params.page at build time.
  • Were using fallback: false because we always want to statically generate these paths at build time. If we add more blog posts which changes the number of pages we need to render, wed want to build the site again. This is usually done with webhooks that Contentful sends to your hosting platform of choice each time you publish a new change. Read more about the fallback key here.

In our dynamic route, were using getStaticProps() in a similar way to /blog, with the only difference being that were using params.page in the calls to the Contentful API instead of hardcoding page number 1.

Now we have our blog post summary data from Contentful, requested at build time and passed to our blog index and dynamic blog pages. Great! Lets build a component to display our posts on the front end.

Building the post list component

Lets build a PostList component that we will use on the blog index and our dynamic routes.

Create a components directory at the route of your project, create a new directory inside that called PostList, and add a new file inside that directory called index.js.

Screenshot of the PostList component directory in VSCode

The PostList renders an ordered list (<ol>) of article elements that display the date, title, tags and excerpt of the post via the JavaScript map() function. We use next/link to enable a client-side transition to the blog post itself. Also notice that were using the post.sys.id on the <li> element to ensure each element in the map has a unique key. Read more about keys in React.

This example uses react-markdown to render the markdown of the excerpt field. This package is an optional dependency. Using it depends on the amount of flexibility you require for displaying formatted text in the blog-post excerpt. If youre curious, you can view the ReactMarkdownRenderers.js file in the example project repository. This is used to add CSS classes and formatting to the markdown returned from the API.

If youd like to use react-markdown with the renderer options provided in the example project, install the package via npm following the given instructions.

Ive also included a couple of date-formatting functions for the HTML <time> element referenced below in this file on GitHub to help you out.

// /components/PostList/index.jsimport Link from "next/link";import ReactMarkdown from "react-markdown";import ReactMarkdownRenderers from "@utils/ReactMarkdownRenderers";import {  formatPublishedDateForDateTime,  formatPublishedDateForDisplay,} from "@utils/Date";export default function PostList(props) {  const { posts } = props;  return (      <ol>        {posts.map((post) => (          <li key={post.sys.id}>            <article>              <time dateTime={formatPublishedDateForDateTime(date)}>                {formatPublishedDateForDisplay(date)}              </time>              <Link href={`blog/${post.slug}`}>                <a>                  <h2>{post.title}</h2>                </a>              </Link>              <ul>                {tags.map((tag) => (                  <li key={tag}>{tag}</li>                ))}              </ul>              <ReactMarkdown                children={post.excerpt}                renderers={ReactMarkdownRenderers(post.excerpt)}              />            </article>          </li>        ))}      </ol>  );}

Render your postList in your BlogIndex and BlogIndexPage components like so. Pass the totalPages and currentPage props in, too, as well be using them in the final part of this guide.

// /pages/blog/index.js// Do the same for /pages/blog/page/[page].jsimport PostList from "@components/PostList";export default function BlogIndex(props) {  const { postSummaries, currentPage, totalPages } = props;  return (        <PostList             posts={postSummaries}             totalPages={totalPages}            currentPage={currentPage}       />  );}

You should now have your post list rendering on /blog and /blog/page/2. Theres one more piece to the puzzle! Lets build a component to navigate back and forth in our pagination.

Building the pagination component

Were going to make our lives really easy here! To ensure that our application can scale nicely and that we dont have to battle with displaying or truncating a million page numbers when weve written a bazillion blog posts, we will be rendering only three UI elements inside our pagination component:

  • A previous page link
  • A current page / total pages indicator
  • A next page link

Inside components/PostList, add a new directory called Pagination. Inside that directory, add a new file called index.js.

A screenshot of the Pagination component directory created in VSCode

Add the following code to index.js.

// /components/PostList/Pagination/index.jsimport Link from "next/link";export default function Pagination(props) {  const { totalPages, currentPage, prevDisabled, nextDisabled } = props;  const prevPageUrl =    currentPage === "2"      ? "/blog"      : `/blog/page/${parseInt(currentPage, 10) - 1}`;  const nextPageUrl = `/blog/page/${parseInt(currentPage, 10) + 1}`;  return (    <ol>      <li>        {prevDisabled && <span>Previous page</span>}        {!prevDisabled && (          <Link href={prevPageUrl}>            <a>Previous page</a>          </Link>        )}      </li>      <li>        Page {currentPage} of {totalPages}      </li>      <li>        {nextDisabled && <span>Next page</span>}        {!nextDisabled && (          <Link href={nextPageUrl}>            <a>Next page</a>          </Link>        )}      </li>    </ol>  );}

Were using the next/link component to make use of client-side routing, and were calculating the links to the next and previous pages based on the currentPage prop.

Import the Pagination component at the top of the PostList file, and add it at the end of the template rendering the HTML. Pass in the totalPages and currentPages props.

Next, calculate the nextDisabled and prevDisabled variables based on the currentPage and totalPages:

  • If were on the page one, prevDisabled = true
  • If were on the last page, nextDisabled = true

Finally, pass these two props to the Pagination component.

// /components/PostList/index.jsimport Pagination from "@components/PostList/Pagination";export default function PostList(props) { // Remember to take the currentPage and totalPages from props passed // from the BlogIndex and BlogIndexPage components  const { posts, currentPage, totalPages } = props; // Calculate the disabled states of the next and previous links  const nextDisabled = parseInt(currentPage, 10) === parseInt(totalPages, 10);  const prevDisabled = parseInt(currentPage, 10) === 1;  return (    <>      // Post list <ol>...      <Pagination        totalPages={totalPages}        currentPage={currentPage}        nextDisabled={nextDisabled}        prevDisabled={prevDisabled}      />    </>  );}

And thats it! Youve built statically generated article list pages based on the number of blog posts in the example Contentful space and how many posts youd like to show per article list page.

The finished product

In this tutorial we built statically generated article list pagination using data from Contentful in a fresh Next.js application. You can find the final styled result here, and heres how it looks.

Screenshot of the blog index page on the Next.js Contentful blog starter example website

If you want to look at how the demo site is styled with CSS, take a look at these files on GitHub.

If youve set up a webhook inside Contentful to trigger a build every time you publish a change, your article list pages will be rebuilt, and continue to generate the /blog/page/{pageNumber} routes dynamically based on how many blog post entries you have!

If youve found this guide useful, Id love for you to come and say hi on Twitch, where I code live three times a week. I built this code on stream!

And remember, build stuff, learn things and love what you do.


Original Link: https://dev.to/contentful/paginating-your-contentful-blog-posts-in-next-js-with-the-graphql-api-10f6

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To