Pagination in Nuxt Content

Debbie O'Brien - Mar 26 '22 - - Dev Community

Nuxt content is by far my favorite feature of Nuxt. It allows me to easily write my blog posts in Markdown format yet add components on to the page if and when I need to. And I love the live edit where I can simply click and edit the text directly in the browser and it saves the code for me. Mind blowing feature.

Writing my content in markdown makes it then easy for me to copy those posts and paste them to my dev.to account with a canonical link to my site. I don't need a content management tool to write my posts and am quite happy using Nuxt content to write my post, push it to GitHub and have Netlify build and publish a new version of my static site.

Why Pagination

As I started to write more posts my blog page was getting very long and even though I had already split the posts into categories some of the categories were also getting quite long. This means the pages where handing a lot of content that needs to be scrolled and a lot of content that needs to be loaded on the page. Adding pagination would make for a better user experience but also better performance.

Querying your Endpoint

The first thing I did was try to get pagination on the main blog page and then worry about getting it to work on the categories. Nuxt content will fetch my markdown files from a directory inside the content folder.

Remember you can test your queries locally by adding _content in your localhost URL and seeing the results of the data returned.

http://localhost:3000/_content/articles?only=title&limit=9&skip=9
Enter fullscreen mode Exit fullscreen mode

Fetching the Posts

To fetch the data we use asyncData passing in $content and params so we can access them from the Nuxt context. Then we add a const of pageNo which will get the number of the page from the params and we use parseInt to convert it to a number. Note: At the moment I am using Nuxt 2 until Nuxt 3 can support Nuxt content.

I want to get 9 Articles per page so we create a const called getArticles and then use the $content method passing in the folder to where my posts are stored. We then add a condition using .where. We want to make sure we only publish posts that do not have published set to false.

const getArticles = await $content('articles').fetch()
Enter fullscreen mode Exit fullscreen mode

Make sure you always add the .fetch() at the end of your query. I have very often forgotten this and wondered why I wasn't getting any data back.

Fetch only posts where published is not false

In my posts I add published: false for those posts that are still a work in progress. That means I can still push them to GitHub but they won't get fetched by Nuxt content until I remove this from the yaml or set published to true. The reason I choose to use not equal to false instead of making it true was to basically not have to go back over all posts and add a condition to publish them.

const getArticles = await $content('articles')
  .where({ published: { $ne: false } })
  .fetch()
Enter fullscreen mode Exit fullscreen mode

Limit the amount of posts returned

Next we want to limit the amount of posts that come back so that we only have 9 posts per page.

const getArticles = await $content('articles')
  .where({ published: { $ne: false } })
  .limit(9)
  .fetch()
Enter fullscreen mode Exit fullscreen mode

Skip the posts based on page number

We then add a condition to skip the first 9 posts times the page number -1 so if on page 1, don't skip any. If on page 2 skip 9 etc. This is because we want to show the first page of posts and then the second page of posts and so on.

const getArticles = await $content('articles')
  .where({ published: { $ne: false } })
  .limit(9)
  .skip(9 * (pageNo - 1))
  .fetch()
Enter fullscreen mode Exit fullscreen mode

Sort the posts by date

Next we sort the posts by date in descending order so that the newest posts are on top.

const getArticles = await $content('articles')
  .where({ published: { $ne: false } })
  .limit(9)
  .skip(9 * (pageNo - 1))
  .sortBy('date', 'desc')
  .fetch()
Enter fullscreen mode Exit fullscreen mode

Set the next page

Next page is set to true if the amount of articles received is equal to 9. This means we can then render our next page button if the condition is true.

const nextPage = getArticles.length === 9
Enter fullscreen mode Exit fullscreen mode

Return what we need

Our final step and one of the most important is to return our data which is our const of getArticles as well as return our nextPage and our pageNo.

return {
  nextPage,
  getArticles,
  pageNo
}
Enter fullscreen mode Exit fullscreen mode

The final code looks something like this. Note I have the layout properties in here so that all my blog pages use the same layout which I named blog. I also added a const called numArticles making it equal to 9 just to keep things dry and finally I added an if statement to deal with errors incase there are no articles returned. This will render my error page with the message of 'no articles found'

export default {
  layout: 'blog',
  async asyncData({ $content, params }) {
    const pageNo = parseInt(params.number)
    const numArticles = 9

    const getArticles = await $content('articles')
      .where({ published: { $ne: false } })
      .limit(numArticles)
      .skip(numArticles * (pageNo - 1))
      .sortBy('date', 'desc')
      .fetch()

    if (!getArticles.length) {
      return error({ statusCode: 404, message: 'No articles found!' })
    }

    const nextPage = getArticles.length === numArticles
    getArticles
    return {
      nextPage,
      getArticles,
      pageNo
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Rendering the posts

The next step is to render the posts. We do this by using v-for and looping over the getArticles and rendering each article using the 'PostsCard' component.

<div v-for="article of getArticles" :key="article.slug" class="flex flex-col">
  <PostsCard :item="article" />
</div>
Enter fullscreen mode Exit fullscreen mode

Rendering the Pagination Component

We then render the pagination component which has a prop of nextPage and a prop of pageNo. We want the first page to be 1 and the nextPage will be either true or false depending on if the length of our articles is equal to 9.

<Pagination :nextPage="nextPage" :pageNo="1" urlPrefix="/blog/all" />
Enter fullscreen mode Exit fullscreen mode

Creating Dynamic Category Pages

We have pagination on the main blog page but now we need to create pages for each category so we can have pagination for the Nuxt category, React category, Testing category etc. In Nuxt we can create dynamic pages by creating a folder with _category and inside it a folder with _number. This will give you a url of /blog/category/number but as it is dynamic it will render something like this /blog/nuxt/1.

We then create an index file inside the _number folder. This will be the page that gets rendered containing the blog posts for that category.

The main difference between this and the main blog page is adding the selectedTag to our data with the value of the category we get back from our route params.

data() {
      return {
        selectedTag: this.$route.params.category
      }
    },

Enter fullscreen mode Exit fullscreen mode

We also need to add a computed property to filter the articles by the selected Tag. Using the .filter() method it will go through each article to see if the selected Tag, which we get from our route params, is found inside the tags array that is added to the yaml of each article. The tags array looks something like this tags: [Nuxt, All].

    computed: {
      filteredArticles() {
        return this.getArticles.filter(article =>
          article.tags.includes(this.selectedTag)
        )
      }
    }
Enter fullscreen mode Exit fullscreen mode

Rendering our filtered posts

Now when rendering our posts we need to use the filteredArticles instead of the getArticles.

<div
  v-for="article of filteredArticles"
  :key="article.slug"
  class="flex flex-col"
>
  <PostsCard :item="article" />
</div>
Enter fullscreen mode Exit fullscreen mode

Rendering the pagination

For our pagination component we need to pass in the prop of prevPage and set it to true of false if the page number is greater than 1. We also pass in our nextPage and pageNo props and finally our urlPrefix which gets our category from the route params.

<Pagination
  :prevPage="pageNo > 1"
  :nextPage="nextPage"
  :pageNo="pageNo"
  :urlPrefix="`/blog/${this.$route.params.category}`"
/>
Enter fullscreen mode Exit fullscreen mode

Conclusion

I now have pagination running on my blog and a page for each category and number. These pages are dynamic and upon building my static site, Nuxt will pre-render a page for each of these dynamic pages. This will enhance performance and give users a much better experience.

There is one thing I am not too happy with. My main blog page is practically a copy of the dynamic index page under the category/number folder. This means I have to maintain this code twice and that is never good. There are a few ways around this.

I could create a middleware that intercepts this route and brings me to the all category page 1 route. I could also create a Netlify redirect that will do the same thing. However I do like having the blog page route of just /blog so I am undecided on what the best solution here is. If you have any suggestions let me know.

Useful Links

My code is fully open source so clone, copy, or use whatever you like. Have fun.

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