5 min read
Adding Pagination to Post Listings with Nuxt Content
When building blogs or content sites, you'll often need to split long lists of posts across multiple pages. Pagination allows breaking up large amounts of data into smaller chunks, improving site performance and user experience. In this tutorial, I'll explain how to add pagination to display blog post listings using the @nuxt/content module in a Nuxt 3 application.
Overview
We'll be paginating the main post list page in our Nuxt blog. Here's what we'll cover:
- Understanding pagination concepts and strategies
- Adding basic previous/next pagination links
- Building a numbered pagination component
- Dynamic page-based querying with
$content
- Generating pages and routes for each page
- Creating and registering a custom pagination component
- Displaying paginated posts across multiple pages
Setting Up the Nuxt Blog
Let's start with the Nuxt blog from the previous tutorial. I'll recap the key pieces we need:
- A
/posts
route to display all posts - Markdown content for blog posts under
content/posts
- Query to get posts in
pages/posts/index.vue
export default {
async asyncData() {
const posts = await queryContent("/posts")
.sortBy("createdAt", "desc")
.find();
return {
posts,
};
},
};
This fetches all posts sorted by recent first. We'll update it to get paginated results.
Pagination Basics
A few concepts before we dive into implementation:
What is Pagination?
Pagination is the process of splitting content across multiple pages, typically with navigation controls to move between pages.
For example, displaying only 10 posts per page and showing links to move between page 1, 2, 3 etc.
Why Pagination?
- Improves performance by reducing content on each page
- Reduces loading time instead of fetching all data
- Enhances user experience for quickly browsing/navigating
- Allows infinite or lazy loading approaches
Pagination Strategies
There are a few common ways pagination is handled:
- Numbered pages - Page 1, 2, 3 etc with previous/next links
- Infinite scroll - Load more content continuously on scroll
- Cursor-based - Use cursor to load next set of items
For this tutorial, we'll focus on the numbered pages strategy with previous/next links.
Adding Previous/Next Links
Let's start simple and add just the previous/next links to move between the first page and second page.
First, we need to update the posts
query to get paginated results from the content module:
// posts/index.vue
export default {
async asyncData() {
const posts = await queryContent("/posts")
.sortBy("createdAt", "desc")
.only(["title", "slug"])
.paginate(1, 10)
.find();
return {
posts,
};
},
};
We use the .paginate()
method to get only the first 10 posts on page 1.
Then in the template, we can access pagination data:
<template>
<div v-for="post in posts">
<!-- display post -->
</div>
<div v-if="posts.pagination">
<NuxtLink
v-if="posts.pagination.prevPageUrl"
:to="posts.pagination.prevPageUrl"
>
Previous
</NuxtLink>
<NuxtLink
v-if="posts.pagination.nextPageUrl"
:to="posts.pagination.nextPageUrl"
>
Next
</NuxtLink>
</div>
</template>
This will display the Next link on the first page, and Prev link on second page. Clicking on them flips between the two sets of 10 posts.
So with just a couple lines of code, we can enable simple back/forth navigation!
Building Numbered Pagination Component
While previous/next links work, it's a more common pattern to show numbered pagination for clear navigation.
Let's build a reusable <Pagination>
component to display page numbers and the active state.
Under components/
, create Pagination.vue
:
<template>
<div class="pagination">
<NuxtLink
v-for="page in pages"
:key="page.url"
:to="page.url"
:class="{ active: page.active }"
>
{{ page.number }}
</NuxtLink>
</div>
</template>
<script>
export default {
props: {
pages: {
type: Array,
default: () => [],
},
},
};
</script>
It simply loops through the pages
prop and displays the page number and active class.
Now we can use it in posts/index.vue
template:
<Pagination :pages="posts.pagination.pageList" />
By passing the pageList
array from the pagination response, it will display page numbers!
Dynamic Page-based Querying
So far we have hardcoded page 1 in the query. But we want each numbered link to fetch the posts for that specific page.
Nuxt Content exposes the $content
helper that provides methods like fetch()
to make API calls to content endpoints. This can be used to dynamically paginate based on page.
First, we need to check the route query in asyncData
to get current page parameter:
// posts/index.vue
export default {
async asyncData(context) {
// get page from query or default to 1
const page = parseInt(context.query.page) || 1;
const posts = await $content("/posts", page)
.sortBy("createdAt", "desc")
.only(["title", "slug"])
.paginate(page, 10)
.find();
return {
posts,
};
},
};
We pass the dynamic page
variable into $content
and .paginate()
to fetch posts for that particular page.
Now the Pagination links will load correct set of posts on each page!
Generating Page Routes
By default, the pagination links point to /posts?page=x
. Instead we want clean URLs like /posts/1
, /posts/2
etc.
Nuxt has a generate
plugin to generate associated routes for each pagination page.
Install it:
npm install @nuxt/content-theme-docs
Then in nuxt.config.js
:
import { defineNuxtConfig } from "nuxt";
export default defineNuxtConfig({
modules: ["@nuxt/content"],
content: {
documentDriven: true,
},
hooks: {
"content:generate": async () => {
await generateRoutes("posts");
},
},
});
const { generateRoutes } = await import("@nuxt/content-theme-docs");
We are enabling documentDriven
mode and calling generateRoutes()
in the hook to generate paginated routes for /posts
.
Now instead of ?page=
queries, we'll have clean URLs with page numbers!
Registering Pagination Component
The pagination component we built won't work by default since Nuxt Content handles rendering the UI.
We need to register it as a custom component override.
Create components/pagination.vue
:
<template>
<Pagination :pages="pagination.pageList" />
</template>
<script>
import Pagination from "~/components/Pagination";
export default {
props: ["pagination"],
};
</script>
Then in nuxt.config.js
:
export default {
content: {
documentDriven: true,
components: {
pagination: "~/components/pagination.vue",
},
},
};
This maps our custom component to override the default!
Now we should see the numbered pagination on /posts
🎉
Displaying Paginated Posts
The final piece is displaying the correct posts across the different pagination pages.
Since content is fetched dynamically based on page, this should mostly work automatically. But let's validate in pages/posts/_page.vue
:
<template>
<div>
<h1>Posts - Page {{ $route.params.page }}</h1>
<div v-for="post in posts">
<!-- render posts here -->
</div>
</div>
<!-- content injected here -->
</template>
We can access the specific page number param from route and display it.
The paginated posts will be injected from the dynamic $content
query.
Now navigate to /posts/1
, /posts/2
etc and confirm correct posts display on each page!
Recap and Next Steps
That concludes a full implementation of pagination for a Nuxt blog using Nuxt Content!
Here's what we covered:
- Pagination concepts and various strategies
- Adding simple previous/next navigation
- Building a custom numbered pagination component
- Using
$content
for dynamic page-based querying - Generating named routes for each page
- Registering and integrating our custom pagination component
- Displaying the correct paginated posts across pages
Of course, there's always more that can be done to enhance pagination:
- Improve styling for active state
- Add ellipses (...) between page numbers
- Implement infinite scrolling
- Optimize queries for faster page transitions
- Cache paginated results to improve performance
I hope you enjoyed learning how to implement pagination in Nuxt. Let me know if you have any other Nuxt or Nuxt Content topics you'd like to see an article on!