Adding pagination into Next.js blog

Pavel Polívka - Nov 11 '21 - - Dev Community

I recently redid my blog with Next.js. I used the amazing Next.js tutorial and I was very happy with it. But as time went on and I wrote more and more articles it became apparent that I need to add paging. I am not an expert on Next and it turns out that adding paging will not be that easy. I used static generation for my listing page and generating all the pages is not an option. I decided to switch to server-side rendering for SEO reasons but also I wanted to switch pages on the fly.

Adding API

First thing I needed to add an API call that would provide paging info and list posts.
I created a posts directory in a root api folder and in there I created a [page].js file. This file will be my api handler.

// api/posts/[page].js

import {getSortedPostsData} from "../../lib/posts";


export default function (req, res) {
    const { page } = req.query
    const allPostsData = getSortedPostsData()
    const perPage = 9
    const totalPosts = allPostsData.length
    const totalPages = totalPosts / perPage
    const start = (page - 1) * perPage
    let end = start + perPage
    if (end > totalPosts) {
        end = totalPosts
    }

    res.status(200).json({
        currentPage: page,
        perPage: perPage,
        totalCount: totalPosts,
        pageCount: totalPages,
        start: start,
        end: end,
        posts: allPostsData.slice(start, end)
    })
}
Enter fullscreen mode Exit fullscreen mode

This is pretty straightforward code. It's doing some stats from an array of all posts.
Side note here, if you are deploying to Vercel, your api calls are deployed as serverless functions and you need to tell Vercel to add your markdown files to the serverless deploy. This is done via root vercel.json file.

{
  "functions": {
    "api/posts/[page].js": {
      "includeFiles": "posts/**"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The root posts directory is the place where I have all the markdown files.

Modifying blog listing page

I used the blog listing page pretty much out of the next.js tutorial. I was using static page generation. So the first thing I have done was to change it to server-side rendering.

Blog.getInitialProps = async ({ query }) => {
    const page = query.page || 1; //if page empty we request the first page
    const response = await fetch(`${server}/api/posts/${page}`)
    const posts = await response.json()
    return {
        totalCount: posts.totalCount,
        pageCount: posts.pageCount,
        currentPage: posts.currentPage,
        perPage: posts.perPage,
        posts: posts.posts,
    }
}
Enter fullscreen mode Exit fullscreen mode

It fetches our new api call and returns it as our component properties.
The server variable is different for localhost and for prod. We need to specify the full path as this will be called from the server.

const dev = process.env.NODE_ENV !== 'production';
export const server = dev ? 'http://localhost:3000' : 'https://ppolivka.com';
Enter fullscreen mode Exit fullscreen mode

I am using next/router to navigate between pages. And to make all the things more user-friendly I added a loading animation on route changes.

const [isLoading, setLoading] = useState(false);
const startLoading = () => setLoading(true);
const stopLoading = () => setLoading(false);

useEffect(() => {
    Router.events.on('routeChangeStart', startLoading);
    Router.events.on('routeChangeComplete', stopLoading);

    return () => {
        Router.events.off('routeChangeStart', startLoading);
        Router.events.off('routeChangeComplete', stopLoading);
    }
}, [])
Enter fullscreen mode Exit fullscreen mode

To render the posts or the loading I have a if in this style.

let content;
if (isLoading) {
    content = (
        <div className={styles.loadWrapper}>
            <Spinner animation="border" role="status">
                <span className="visually-hidden">Loading...</span>
            </Spinner>
        </div>
    )
} else {
    //Generating posts list
    content = (
        <>
            {props.posts.map(({ id, date, title, image, description }) => (
                <Card className={styles.item}>
                    <Card.Img variant="top" src={image} width={360} height={215} />
                    <Card.Body>
                        <Card.Title>
                            <Link href={`/posts/${id}`}>
                                <a>
                                    {title}
                                </a>
                            </Link>
                        </Card.Title>
                        <Card.Subtitle className="mb-2 text-muted"><Date dateString={date} /></Card.Subtitle>
                        <Card.Text>
                            {description}
                        </Card.Text>
                    </Card.Body>
                </Card>
            ))}
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

For the actual pagination navigation I used awesome component react-paginate.

<ReactPaginate
    previousLabel={'<'}
    nextLabel={'>'}
    breakLabel={'...'}
    breakClassName={'break-me'}
    activeClassName={'active'}
    containerClassName={'pagination'}
    subContainerClassName={'pages pagination'}
    initialPage={props.currentPage - 1}
    pageCount={props.pageCount}
    marginPagesDisplayed={2}
    pageRangeDisplayed={5}
    onPageChange={paginationHandler}
/>
Enter fullscreen mode Exit fullscreen mode

It's referring the pagination handler function, that has the actual navigation logic.

const paginationHandler = (page) => {
    const currentPath = props.router.pathname;
    const currentQuery = props.router.query;
    currentQuery.page = page.selected + 1;

    props.router.push({
        pathname: currentPath,
        query: currentQuery,
    })

}
Enter fullscreen mode Exit fullscreen mode

You can see the whole blog page in this Gist.


If you like this article you can follow me on Twitter.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .