Pagination with Nuxt-Content
Website Monitoring Magazine
In the previous article, I showed you how to add a categorization to the magazine. This sets the course for an expansion of the content offer. However, this creates a problem, that of the loading time. You don't want to wait until hundreds of articles are collected, filtered and then output, smaller bites are better for the loading time.
Number of articles
For the pagination to work, the total number of all articles is needed.
Total number of articles / articles per page = number of paginated pages
This is not a big problem, because after the query, you only have to count the number of delivered items. But if you limit the number (to keep the resulting JSON small), because you want to display only nine items as in the case below, the total number consists of only nine or less items.
const articles = await this.$content(this.path, { deep: true })
.only(['title', 'description', 'slug', 'type', 'image', 'dir', 'path', 'author'])
.limit(9)
.fetch()
You could remove the limit altogether, but that wouldn't make sense. So what remains? One sends an additional request, which collects all articles of the filter in question, independent of a limit. Since only([])
excludes all other information, you get an array with many empty arrays, from which you then take the total number.
const articleCount = (await this.$content(this.path, { deep: true })
.only([])
.fetch())
.length
Problem solved? Almost...
Reduction of the requests
In the upper case, you now send two asynchronous calls that retrieve all articles (without further details) and then the last 9 (with more details). The purpose behind the query is to keep the resulting JSON as small as possible. This doesn't matter so much with 20 or 30 articles, but with 100 or more, the whole thing works more and more against the performance.
The whole thing adds up if you filter the article previews by categories, authors or tags. There is then not only a number of all articles, but also a number of articles that fall under the category 'plate edge'.
So there must be a kind of cache, which stores the number of articles of the selected filter. So the first time a category overview is called it will still send two queries (once for the count and then for the articles), but the second time the count query is omitted.
Vuex Store
To store the count of the articles, a store is created with Vuex.
export const state = () => ({
polling: true,
counts: {},
paginatedCount: 12
})
export const mutations = {
SET_POLLING (state, status) {
state.polling = status
},
SET_COUNT (state, { type, count }) {
state.counts[type] = count
}
}
export const actions = {
setCount ({ commit, state }, { type, count }) {
commit('SET_COUNT', { type, count })
},
setPolling ({ commit }, status) {
commit('SET_POLLING', status)
}
}
export const getters = {
getPolling: state => state.polling,
getCount: state => state.counts,
getPaginatedCount: state => state.paginatedCount
}
This is kept relatively simple. The article count is stored in an object counts
and then returned via getCount
. By association with the respective filter, one can query on the component side whether the corresponding count is available.
counts: {
agenturleben: 8,
tellerrand: 3,
nils: 7
}
__What you might notice is that I don't split by author and category again, I'm good that we don't name categories like authors do.
I also stored the number of articles per page in the store, this will be needed later for the 'ArticleCollection' and the 'Pagination'.
On top of that there is the polling that will trigger the Computed Properties within the Paginations components....
Cache function and article selection
The ArticleCollection, is used to collect the article previews. The component is used on the home page, the category page and the author page, so it is guaranteed that it is in action for each of the possible filters. And is therefore predestined to initialize the cache.
import { mapGetters } from 'vuex'
import ArticlePreview from '@/components/notice/ArticlePreview/ArticlePreview'
export default {
name: 'ArticleCollection',
components: { ArticlePreview },
props: {...},
async fetch () {
const { query } = this.$route
const pageNumber = query.p ? query.p : 1
this.error = false
const baseQuery = () => ({ type: { $ne: 'category' } })
const publishQuery = () => process.env.NODE_ENV === 'production' ? ({ published: true }) : null
const authorQuery = () => this.authorArticles ? ({ author: { $eq: this.$route.params.author } }) : null
const limiter = this.asident
? 1
: this.paginated
? this.paginatedCount
: this.limit
const skipper = this.paginated
? this.paginatedCount * (pageNumber - 1)
: null
try {
if (this.paginated) {
this.$store.dispatch('magazine/setPolling', true)
const path = this.authorArticles ? this.$route.params.author : this.path.split('/')[2]
if (!this.getCount[path]) {
const articleCount = (await this.$content(this.path, { deep: true })
.only([])
.where({ ...baseQuery(), ...publishQuery(), ...authorQuery() })
.fetch())
.length
this.$store.dispatch('magazine/setCount', { type: path, count: articleCount })
}
this.$store.dispatch('magazine/setPolling', false)
}
const articles = await this.$content(this.path, { deep: true })
.only(['title', 'description', 'slug', 'type', 'image', 'dir', 'path', 'author'])
.where({ ...baseQuery(), ...publishQuery(), ...authorQuery() })
.sortBy('createdAt', this.sortDirection)
.skip(skipper)
.limit(limiter)
.fetch()
this.articles = articles
.filter(val => this.activeArticle.length ? val.title !== this.activeArticle : val)
} catch (error) {
console.error(error)
this.error = true
}
},
data () {
return {
articles: null,
error: false
}
},
computed: {
...mapGetters({
getCount: 'magazine/getCount',
paginatedCount: 'magazine/getPaginatedCount'
})
},
watch: {
'$route.query': '$fetch'
}
}
If you have looked at the last version, you will have noticed the extension, I will now go into the details. Since you set the page number by query parameters, it is of course useful to save the query, if necessary to set a default value of 1.
async fetch() {
const { query } = this.$route
const pageNumber = query.p ? query.p : 1
...
}
Whether the entries are paginated depends on whether the Prop paginated
was set. If this is the case, the limit stored in the store paginatedCount
is used.
const limiter = this.asident
? 1
: this.paginated
? this.paginatedCount
: this.limit
But now it gets really interesting, because with the value for skip
, we determine how many articles are skipped in the query. Again, it is only set when pagination is enabled. For example, if 12 articles are set per page and we are on page 3, 24 articles will be skipped.
const skipper = this.paginated
? this.paginatedCount * (pageNumber - 1)
: null
Now we come to the triggering of the cache, which is in the try-catch
block. Not much exciting happens there, it's just that we can store the number of items. If we already have the number, not much more happens here.
if (this.paginated) {
const path = this.authorArticles
? this.$route.params.author
: this.path.split('/')[2]
if (!this.getCount[path]) {
this.$store.dispatch('magazine/setPolling', true)
const articleCount = (await this.$content(this.path, { deep: true })
.only([])
.where({ ...baseQuery(), ...publishQuery(), ...authorQuery() })
.fetch())
.length
this.$store.dispatch('magazine/setCount', { type: path, count: articleCount })
this.$store.dispatch('magazine/setPolling', false)
}
}
Last but not least, in addition to the obligatory mapGetters
, there is a watcher that monitors the route. If something changes in the queries, it will trigger a new fetch()
and thus re-render the view.
watch: {
'$route.query': '$fetch'
}
Pagination
The store is used to get the data necessary to build the pagination. In the case of Koality there is only a pagination if a category is used or you want to display the articles of an author.
The computed property pages()
is updated whenever something changes in the path. This saves several watchers
. Otherwise, although the component is the core, it is kept relatively simple.
<template>
<div v-if="pages > 1">
<nav class="flex items-center justify-center -mx-1">
<div
v-for="page in pages"
:key="page"
class="px-1"
>
<a
class="h-8 w-8 border border-gray-200 block flex-center cursor-pointer "
:class="[page === parseInt(current) ? 'bg-gray-600 text-white' : 'text-gray-600']"
@click.prevent="$router.push({ query: { p: page }})"
>
{{ page }}
</a>
</div>
</nav>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'MagazinPagination',
props: {
path: {
type: String,
default: '/articles'
},
authorArticles: {
type: Boolean,
default: false
}
},
computed: {
...mapGetters({
getCount: 'magazine/getCount',
paginatedCount: 'magazine/getPaginatedCount',
polling: 'magazine/getPolling'
}),
current () {
const { query } = this.$route
return query.p ? query.p : 1
},
pages () {
if (this.polling) { return }
const path = this.authorArticles
? this.$route.params.author
: this.path.split('/')[2]
const pages = Math.round(this.getCount[path] / this.paginatedCount)
return pages > 0 ? pages : 1
}
}
}
</script>