improving navigation and adding archive lists
gatsby blogging: part 4added 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>
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.
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.