Collections & Pagination
The Collection built-in component proxies any resource collection endpoint. On the front-end, useCwaComponent with the withCollection() plugin unwraps the Hydra response and gives you items, pagination state, and navigation helpers — all URL-bound.
Display Component
<!-- app/app/cwa/components/BlogList/BlogList.vue -->
<template>
<div>
<div v-if="isLoadingCollection" class="space-y-4">
<div v-for="n in 6" :key="n" class="h-48 bg-gray-100 animate-pulse rounded" />
</div>
<div v-else-if="collectionItems" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ArticleCard
v-for="item in collectionItems"
:key="item['@id']"
:article="item"
:to="resolveResourceLink(item, 'routePath')"
/>
</div>
<div v-if="totalPages > 1" class="flex items-center justify-center gap-4 mt-8">
<button
:disabled="!pageModel || pageModel <= 1"
class="btn"
@click="goToPreviousPage"
>
Previous
</button>
<span class="text-sm text-gray-600">
Page {{ pageModel }} of {{ totalPages }}
</span>
<button
:disabled="pageModel >= totalPages"
class="btn"
@click="goToNextPage"
>
Next
</button>
</div>
</div>
</template>
<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent, withCollection } from '#imports'
const props = defineProps<IriProp>()
const {
resource,
exposeMeta,
collectionItems,
isLoadingCollection,
totalPages,
pageModel,
goToNextPage,
goToPreviousPage,
resolveResourceLink
} = useCwaComponent(props, [withCollection()])
defineExpose(exposeMeta)
</script>
Return Values
useCwaComponent › withCollection() lists everything the plugin returns. Two things to remember here: collectionItems holds raw API resources, so read item.title rather than item.data.title, and it is undefined until the first response arrives, so guard your list with v-if="collectionItems".
Numbered Pagination
useCwaCollectionPagination returns a sliding window of page numbers — at most maxPagesToDisplay (default 7), centred on the current page. They are always plain numbers; there are no ellipsis markers.
It takes a single props object of plain numbers, which makes it a natural fit for a dedicated pagination child component:
<!-- app/app/components/CollectionPagination.vue -->
<template>
<nav class="flex gap-1">
<button
v-for="page of pages"
:key="page"
:class="page === currentPage ? 'font-bold' : ''"
@click="$emit('change', page)"
>
{{ page }}
</button>
</nav>
</template>
<script setup lang="ts">
import { useCwaCollectionPagination } from '#cwa/composables/cwa-collection-pagination'
import type { CwaPaginationEmits, CwaPaginationProps } from '#cwa/composables/cwa-collection-pagination'
const props = defineProps<CwaPaginationProps>() // { currentPage, totalPages, maxPagesToDisplay }
defineEmits<CwaPaginationEmits>() // next, previous, change
const { pages } = useCwaCollectionPagination(props)
</script>
Then drop it into the collection component in place of the Previous/Next buttons:
<CollectionPagination
:current-page="pageModel || 1"
:total-pages="totalPages"
:max-pages-to-display="7"
@next="goToNextPage"
@previous="goToPreviousPage"
@change="changePage"
/>
Linking to a Resource's Page
resolveResourceLink(item, property) — destructured from useCwaComponent(props, [withCollection()]) above — returns item[property] when the item carries its own path (e.g. routePath on a published page-data resource), and otherwise a Vue Router location targeting CWA's internal resource-page route.
<CwaLink :to="resolveResourceLink(item, 'routePath')">{{ item.title }}</CwaLink>
:to, not :href — the fallback is a route location object, not a string. Pass the property name too; without it the helper always falls through to the internal route.This is how you turn a collection item into a link without knowing its URL in advance — including draft entries that have no public route yet.
User-Controlled Sorting and Filtering
useQueryBoundModel binds any query parameter to the URL, and the collection re-fetches when it changes. For a sort on one field, bind the bracketed parameter itself:
import { useQueryBoundModel } from '#imports'
const { model: sortOrder } = useQueryBoundModel('order[createdAt]', { defaultValue: 'desc' })
<select v-model="sortOrder">
<option value="desc">Newest first</option>
<option value="asc">Oldest first</option>
</select>
For a text search, bind the parameter and debounce it:
const { model: searchModel } = useQueryBoundModel('search', { delay: 250 })
To let visitors choose the sort field as well as the direction, pass the base key 'order' and the model becomes an object. See useQueryBoundModel for both forms and the options.
The parameter names must match what the API resource declares. With the search and order[:property] parameters from Page Data & Nested Pages › Searching and Sorting, use search for the text box. A per-field name such as title is ignored, so the list would never narrow.
The collection re-fetches automatically when the URL changes. Default query parameters set in the PHP Collection entity are the baseline; useQueryBoundModel lets users override them.
Admin: Setting Up a Collection
The Collection entity comes from the bundle, but its manager tab does not — neither the bundle nor the module ships one. Add it as an app-side admin tab, as the template does at app/app/cwa/components/Collection/admin/CollectionTab.vue, binding the entity's three fields with useCwaResourceModel:
resourceIri— which collection endpoint to list (the template offers a fixed select of endpoints, e.g./page_data/blog_article_datas)perPage— how many items per pagedefaultQueryParameters— default sort and filter values