Posted Tuesday, September 26, 2023
Blog Pagination and Tag Filtering with Nuxt Content v2
How I built pagination and tag archives for this blog.
Nuxt Content makes it easy to build a site out of markdown files. Stangely there are no components or composables to provide pagination of articles or a way to list article tags (taxonomy). The Nuxt Content team has provided the building blocks to do this though. However, most articles I found are for Nuxt Content v1 and this site is using v2 (Nuxt 3 and Vue 3). There are big differences between v1 and v2 so hopefully someone googles this and finds it useful.
Routes
Here is the routing structure I wanted to use for the blog section of this site:
pages
blog
(folder)index.vue
(blog archive page 1)[slug].vue
(blog article renderer)page
(folder for pagination)[[number]].vue
(blog archive pages 2 and up)
tag
(folder)[tag]
(folder)[[number]].vue
(renders blog archive that contains the tag)
Translated to urls on our site:
- /blog/ will be the blog listing
- /blog/page/2 is page 2 of the blog listing
- /blog/this-is-an-article is an article
- /blog/tag/example-tag is the blog listing with the tag of example-tag
- /blog/tag/example-tag/2 is page 2 of the listing of this tag
Pagination
We'll need to know which paginated page we're on by looking at the Nuxt route parameters, the total number of (published) blog posts there are. We'll use the queryContent
composable to do look for all the posts in the blog
folder.
In our /pages/blog/index.vue
(we'll use this same code in our [[number]].vue
pages):
// /pages/blog/index.vue
const currentPage = parseInt(useRoute().params.number) || 1 // [[number]].vue
const allPosts = await queryContent('blog') // look in our /content/blog folder
.where({ published: { $ne: false } })
.without('body') // we don't need any content
.find()
const total = allPosts.length
Now we can start paginating. We'll set the number of posts per page as 12 (this can be any number you want) and then we can figure how many pagnination pages we need. Then we can do another query to get posts using those parameters:
// /pages/blog/index.vue (continued)
const postsPerPage = 12
const pages = Math.ceil(total/postsPerPage) // the number of paginated pages
const posts = await queryContent('blog')
.where({ published: { $ne: false } })
.skip((currentPage - 1) * postsPerPage) // skip posts depending on which paginated page we're on
.limit(postsPerPage)
.without('body')
.sort({date: -1})
.find()
Tags
We’re now going to build a tag list component. It's job is to list all the tags used on our blog posts and link to a blog archive page consisting a list of articles using that tag.
Let's go back to our block of code that gets a list of all the blog posts:
// /pages/blog/index.vue
const currentPage = parseInt(useRoute().params.number) || 1 // blog/page/[[number]].vue or blog/tag/[tag]/[[number]].vue
const currentTag = useRoute().params.tag // blog/[tag]/ folder
let path = 'blog/page'
if (currentTag) path = `blog/tag/${currentTag}` // change route if a tag archive url
const allPosts = await queryContent('blog') // look in our /content/blog folder
.where({ published: { $ne: false } })
.only(['tags']) // only get the tags - we don't need anything else
.find()
const total = allPosts.length
We’re getting the tag parameter and saving it to currentTag
. Now we need to alter the path of our pagination links to either the blog pagination url (no tag selected) or to the tag pagination url. We save this in path
. We've replaced the without
method to only(['tags'])
. Now when the query runs we'll still get an array of all our posts but the content of each post will only have an array of tags attached to the post. We now have all the tags used in all our blog articles.
// allPosts sample output
[
{
"tags":[
"a11y",
"ui"
]
},
{
"tags":[
"side-hustle"
]
},
{
"tags":[
"vue",
"nuxt",
"react",
"svelte"
]
},
{
"tags":[
"css",
"ui"
]
}
]
We need to turn this into a simple array of strings:
// Flatten array of objects of arrays to just an array:
let tags = allPosts.map(obj => obj.tags).flat()
let currentTagCount = 0
// Remove dups, count currentTag occurances and sort alphabetically:
tags = tags.filter((tag, index) => {
const tagIsIn = tags.indexOf(tag) === index
currentTagCount += tag == currentTag
return tagIsIn
}).sort()
Sources
- https://debbie.codes/blog/pagination-in-nuxt-content/ - concepts
- https://stackoverflow.com/a/74235185 - routes for multiple params
- https://redfern.dev/articles/adding-pagination-nuxt-content-blog/ - concepts and redirect path to
page/1
to avoid copying and pasting page template code - https://content.nuxtjs.org/api/composables/query-content - Nuxt
queryContent
reference