improving navigation and adding archive lists

gatsby blogging: part 4

added 16th Jun 2023

As we're adding more posts to the site, let’s improve the navigation in the site to easily move between posts and list posts beyond what the homepage can show.

previous and next links

The first improvement we can make is to add some previous and next links into the post pages.

Within the function that is used to create these post pages, we update the GraphQL query to also fetch the IDs of the next and previous posts in the sequence. These IDs get passed to the post template.

// gatsby-node.js
// exports.createPages()

const results = await graphql(`
  {
    allContentfulPost(sort: { date: ASC }) {
      edges {
        node {
          id
          path
          pathIdOnly
        }
        next {
          id
        }
        previous {
          id
        }
      }
    }
  }
`);
if (results.data.allContentfulPost?.edges?.length) {
  results.data.allContentfulPost.edges.forEach(
    ({ node: post, next, previous }) => {
      createPage({
        path: post.path,
        component: PostTemplate,
        context: {
          id: post.id,
          nextId: next?.id,
          previousId: previous?.id,
        },
      });
      createRedirect({
        fromPath: `${post.pathIdOnly}/*`,
        toPath: post.path,
      });
    },
  );
}

Within the post template, we take these IDs and update the page query to fetch some basic information of the next and previous posts.

// src/templates/PostTemplate.js

export const query = graphql`
  query Post($id: String!, $nextId: String, $previousId: String) {
    contentfulPost(id: { eq: $id }) {
      id
      contentful_id
      title
      date(formatString: "Do MMM YYYY")
      content {
        childMarkdownRemark {
          htmlAst
        }
      }
    }
    nextPost: contentfulPost(id: { eq: $nextId }) {
      id
      contentful_id
      title
      date(formatString: "Do MMM YYYY")
      path
    }
    previousPost: contentfulPost(id: { eq: $previousId }) {
      id
      contentful_id
      title
      date(formatString: "Do MMM YYYY")
      path
    }
  }
`;

From there, all we need to do is add navigation buttons for the next and previous posts to the bottom of the page.

// src/templates/PostTemplate.js

<div className="mt-3 flex">
  {previousPost && (
    <Link
      to={previousPost.path}
      className="group flex max-w-fit items-center gap-1"
    >
      <ChevronLeftIcon className="h-6 w-6 fill-indigo-700 group-hover:fill-indigo-800" />
      <span className="flex max-w-fit flex-col">
        <h4 className="text-md text-indigo-700 decoration-1 underline-offset-2 group-hover:text-indigo-800 group-hover:underline">
          {previousPost.title}
        </h4>
        <p className="text-sm">{previousPost.date}</p>
      </span>
    </Link>
  )}
</div>
<div className="mt-3 flex justify-end">
  {nextPost && (
    <Link
      to={nextPost.path}
      className="group flex max-w-fit items-center justify-end gap-1"
    >
      <span className="flex max-w-fit flex-col text-end">
        <h4 className="text-md text-indigo-700 decoration-1 underline-offset-2 group-hover:text-indigo-800 group-hover:underline">
          {nextPost.title}
        </h4>
        <p className="text-sm">{nextPost.date}</p>
      </span>
      <ChevronRightIcon className="h-6 w-6 fill-indigo-700 group-hover:fill-indigo-800" />
    </Link>
  )}
</div>

post with next and previous

archive pages

The home page of this blog should only display a limited number of posts, usually the most recent ones. For older posts we create an archive that can have multiple pages that list out these posts.

Creating these archive pages is similar to creating the individual post pages, using the createPages function. In this case, we take the total number of posts, and feed it into a function that generates multiple pages with a set limit of posts for each page.

Like posts, there is a template for each archive page that is provided the details to determine the current page, total number of pages, and which posts should be shown on each page.

// gatsby-node.js

const createPagination = ({
  length,
  limit,
  path,
  component,
  context,
  createPage,
  createRedirect,
}) => {
  const total = Math.ceil(length / limit);
  for (var i = 0; i < total; i++) {
    createPage({
      path: `${path}/${i + 1}`,
      component: component,
      context: {
        limit: limit,
        skip: i * limit,
        total: total,
        index: i,
        current: i + 1,
        ...context,
      },
    });
  }
  createRedirect({
    fromPath: `${path}/*`,
    toPath: `${path}/1`,
  });
};

const createArchivePages = async ({ graphql, createPage, createRedirect }) => {
  const ArchiveTemplate = path.resolve('./src/templates/ArchiveTemplate.js');
  const results = await graphql(`
    {
      allContentfulPost(sort: { date: DESC }) {
        edges {
          node {
            id
          }
        }
      }
    }
  `);
  if (results.data.allContentfulPost?.edges?.length) {
    createPagination({
      length: results.data.allContentfulPost.edges.length,
      limit: 5,
      path: '/posts',
      component: ArchiveTemplate,
      createPage: createPage,
      createRedirect: createRedirect,
    });
  }
};

In the archive page template, all we need to do is fetch the posts that should be shown on the current page (based on the page index and posts per page). Then they can be displayed in a post list similar to the home page.

// src/templates/ArchiveTemplate.js

import React from 'react';
import { graphql } from 'gatsby';

import Layout from '../components/layouts/Layout';
import PostListItem from '../components/post/PostListItem';
import PaginationNav from '../components/base/PaginationNav';

const ArchiveTemplate = ({ pageContext, data }) => {
  const { total, index } = pageContext;
  const { edges: posts } = data.allContentfulPost;

  return (
    <Layout>
      <h1 className="mb-6 border-b border-b-indigo-800 pb-3 text-3xl text-indigo-800">
        all posts
      </h1>
      <div className="flex flex-col gap-6">
        {posts &&
          posts.map(({ node: post }) => (
            <PostListItem key={post.id} post={post} />
          ))}
      </div>
      <div className="mt-6 border-t border-t-indigo-800">
        <PaginationNav className="mt-6" total={total} index={index} />
      </div>
    </Layout>
  );
};

export const query = graphql`
  query Posts($limit: Int!, $skip: Int!) {
    allContentfulPost(sort: { date: DESC }, limit: $limit, skip: $skip) {
      edges {
        node {
          id
          contentful_id
          title
          date(formatString: "Do MMMM YYYY")
          description {
            description
          }
          path
        }
      }
    }
  }
`;

export default ArchiveTemplate;

For this archive page, we also include a basic component to show pagination buttons and allow the user to navigate back and forth between the archive pages.

// src/components/base/PaginationNav.js

import React from 'react';
import { Link } from 'gatsby';
import classNames from 'classnames';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/20/solid';

const PaginationNav = ({ className, total, index }) => {
  const Previous = index > 0 ? Link : 'span';
  const Next = index < total - 1 ? Link : 'span';
  return (
    <div className={classNames('flex justify-center', className)}>
      <nav className="flex max-w-fit justify-center divide-x divide-indigo-800 overflow-clip rounded border border-indigo-800">
        <Previous
          className={classNames('group box-content p-2', {
            'hover:bg-indigo-800/90': index > 0,
            'select-none': index === 0,
          })}
          to={`/posts/${index}`}
        >
          <ChevronLeftIcon
            className={classNames('h-6 w-6', {
              'fill-indigo-800 group-hover:fill-white': index > 0,
              'select-none fill-indigo-800/50': index === 0,
            })}
          />
        </Previous>
        {Array.from({ length: total }, (___, i) => {
          const PageLink = index === i ? 'span' : Link;
          return (
            <PageLink
              key={i}
              className={classNames(
                'box-content px-4 py-2',
                'text-indigo-800',
                {
                  'hover:bg-indigo-800/90 hover:text-white': i !== index,
                  'select-none bg-indigo-800/10': i === index,
                },
              )}
              to={`/posts/${i + 1}`}
            >
              {i + 1}
            </PageLink>
          );
        })}
        <Next
          className={classNames('group box-content p-2', {
            'hover:bg-indigo-800/90': index < total - 1,
            'select-none': index === total - 1,
          })}
          to={`/posts/${index + 2}`}
        >
          <ChevronRightIcon
            className={classNames('h-6 w-6', {
              'fill-indigo-800 group-hover:fill-white': index < total - 1,
              'select-none fill-indigo-800/50': index === total - 1,
            })}
          />
        </Next>
      </nav>
    </div>
  );
};

export default PaginationNav;

With all that done, the archive pages look like the below.

post archive page

post archive page 2

home page update

The final little update to make is update the home page with an appropriate limit, and add a link to the archive pages to view older posts.

home page with link to archive