Collection Page
To show products to your customers, you may want to retrieve a collection and display it in your store. This recipe will show you how to create a collection page for your Shopify store in Nuxt.
Accessories



<script setup lang="ts">
const route = useRoute()
const key = `collection-${route.params.handle}`
const perPage = 4
const { data: collection } = await useStorefrontData(key, `#graphql
query GetCollection(
$handle: String!,
$first: Int,
$last: Int,
$after: String,
$before: String,
$language: LanguageCode,
$country: CountryCode
)
@inContext(language: $language, country: $country) {
collection(handle: $handle) {
...CollectionFields
products(
first: $first,
last: $last,
after: $after,
before: $before
) {
...ProductConnectionFields
}
}
}
${COLLECTION_FRAGMENT}
${PRODUCT_CONNECTION_FRAGMENT}
`, {
variables: {
handle: route.params.handle,
language: 'EN',
country: 'US',
first: route.query.before ? undefined : perPage,
last: route.query.before ? perPage : undefined,
after: route.query.after,
before: route.query.before,
},
transform: data => data.collection,
watch: [ () => route.query ],
})
const products = computed(() => flattenConnection(collection.value?.products))
const startCursor = computed(() => collection.value?.products.pageInfo.startCursor)
const endCursor = computed(() => collection.value?.products.pageInfo.endCursor)
const hasPreviousPage = computed(() => collection.value?.products.pageInfo.hasPreviousPage)
const hasNextPage = computed(() => collection.value?.products.pageInfo.hasNextPage)
</script>
<template>
<div>
<p class="mb-4 text-2xl font-bold">
{{ collection?.title }}
</p>
<p class="mb-6">
{{ collection?.description }}
</p>
<div class="grid gap-4 grid-cols-1 sm:gap-8 md:grid-cols-2">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
/>
</div>
<div class="flex justify-between mt-8">
<UButton
v-if="hasPreviousPage"
:to="`?before=${startCursor}`"
icon="i-lucide-arrow-left"
>
Previous
</UButton>
<UButton
v-if="hasNextPage"
:to="`?after=${endCursor}`"
trailing-icon="i-lucide-arrow-right"
>
Next
</UButton>
</div>
</div>
</template>
In order to follow this recipe, you need to have at least one collection with products set up in your Shopify store. Make sure you have configured the Nuxt Shopify module and have a Shopify store set up.
Setting Up GraphQL Fragments
Before we start, let's define some reusable GraphQL fragments for our collection and product data. These fragments will keep our queries clean and make it easy to reuse the same fields across multiple queries.
The collection fragment fetches the basic collection info:
export const COLLECTION_FRAGMENT = `#graphql
fragment CollectionFields on Collection {
id
title
description
}
`
The product fragment fetches the fields we need for each product card:
export const PRODUCT_FRAGMENT = `#graphql
fragment ProductFields on Product {
id
handle
title
description
featuredImage {
url
altText
height
width
}
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
`
And the product connection fragment wraps the product fragment with pagination info:
export const PRODUCT_CONNECTION_FRAGMENT = `#graphql
fragment ProductConnectionFields on ProductConnection {
edges {
cursor
node {
...ProductFields
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
${PRODUCT_FRAGMENT}
`
Fetching the Collection
Now, let's create a page at app/pages/collections/[handle].vue.
We use the useStorefrontData composable to fetch a collection by its handle from Shopify.
The GraphQL query takes a handle variable and returns the collection along with its products:
query GetCollection($handle: String!, $first: Int) {
collection(handle: $handle) {
...CollectionFields
products(first: $first) {
...ProductConnectionFields
}
}
}
When using this query with the useStorefrontData composable, we pass the route param as the handle and use the transform option to extract the collection directly:
<script setup lang="ts">
const route = useRoute()
const { data: collection } = await useStorefrontData(`collection-${route.params.handle}`, `#graphql
query GetCollection($handle: String!, $first: Int) {
collection(handle: $handle) {
...CollectionFields
products(first: $first) {
...ProductConnectionFields
}
}
}
${COLLECTION_FRAGMENT}
${PRODUCT_CONNECTION_FRAGMENT}
`, {
variables: {
handle: route.params.handle,
first: 4,
},
transform: data => data.collection,
})
</script>
<template>
<div>
<p>{{ collection?.title }}</p>
<pre>{{ collection?.products }}</pre>
</div>
</template>
Displaying the Products
The products come back as a Shopify connection (with edges and nodes).
To make them easier to work with, we can use the flattenConnection utility provided by the module to get a flat array of products:
<script setup lang="ts">
// ... useStorefrontData call from above
const products = computed(() => flattenConnection(collection.value?.products))
</script>
<template>
<div>
<p class="mb-4 text-2xl font-bold">
{{ collection?.title }}
</p>
<p class="mb-6">
{{ collection?.description }}
</p>
<div class="grid gap-4 grid-cols-1 sm:gap-8 md:grid-cols-2">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
/>
</div>
</div>
</template>
The ProductCard component receives a product and formats the data for display.
We use Intl.NumberFormat to properly format the price with its currency:
<script setup lang="ts">
import type { ProductFieldsFragment } from '#shopify/storefront'
const props = defineProps<{
product: ProductFieldsFragment
}>()
const price = computed(() => {
const price = props.product.priceRange.minVariantPrice
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: price.currencyCode,
}).format(Number(price.amount))
})
const image = computed(() => props.product.featuredImage)
const to = computed(() => `/products/${props.product.handle}`)
</script>
<template>
<UPageCard
:to="to"
:title="props.product.title"
:description="props.product.description"
orientation="vertical"
reverse
>
<template #header>
<NuxtImg
:src="image?.url"
:alt="image?.altText ?? undefined"
:width="image?.width ?? undefined"
:height="image?.height ?? undefined"
/>
</template>
<template #footer>
<div class="flex justify-between items-center w-full">
<div class="font-semibold">
from {{ price }}
</div>
<UButton size="sm">
Add to Cart
</UButton>
</div>
</template>
</UPageCard>
</template>
Adding Pagination
Shopify uses cursor-based pagination for connections.
We can support this by passing first, last, after and before variables to the query
and reading the cursor values from the route query parameters.
We also need to add the watch option so the data refreshes whenever the user navigates between pages:
<script setup lang="ts">
const route = useRoute()
const key = `collection-${route.params.handle}`
const perPage = 4
const { data: collection } = await useStorefrontData(key, `#graphql
query GetCollection(
$handle: String!,
$first: Int,
$last: Int,
$after: String,
$before: String,
$language: LanguageCode,
$country: CountryCode
)
@inContext(language: $language, country: $country) {
collection(handle: $handle) {
...CollectionFields
products(
first: $first,
last: $last,
after: $after,
before: $before
) {
...ProductConnectionFields
}
}
}
${COLLECTION_FRAGMENT}
${PRODUCT_CONNECTION_FRAGMENT}
`, {
variables: {
handle: route.params.handle,
language: 'EN',
country: 'US',
first: route.query.before ? undefined : perPage,
last: route.query.before ? perPage : undefined,
after: route.query.after,
before: route.query.before,
},
transform: data => data.collection,
watch: [
() => route.query.after,
() => route.query.before,
],
})
const products = computed(() => flattenConnection(collection.value?.products))
const startCursor = computed(() => collection.value?.products.pageInfo.startCursor)
const endCursor = computed(() => collection.value?.products.pageInfo.endCursor)
const hasPreviousPage = computed(() => collection.value?.products.pageInfo.hasPreviousPage)
const hasNextPage = computed(() => collection.value?.products.pageInfo.hasNextPage)
</script>
The pagination logic works as follows:
- When navigating forward, we pass
first(items per page) andafter(the end cursor of the current page). - When navigating backward, we pass
last(items per page) andbefore(the start cursor of the current page).
In the template, we use the pageInfo to conditionally render previous and next buttons:
<template>
<div>
<!-- collection title, description, product grid... -->
<div class="flex justify-between mt-8">
<UButton
v-if="hasPreviousPage"
:to="`?before=${startCursor}`"
icon="i-lucide-arrow-left"
>
Previous
</UButton>
<UButton
v-if="hasNextPage"
:to="`?after=${endCursor}`"
trailing-icon="i-lucide-arrow-right"
>
Next
</UButton>
</div>
</div>
</template>
And that's it! You now have a fully functional collection page with pagination that fetches its data from Shopify.
You can customize the number of products per page by changing the perPage variable, and the page will automatically handle forward and backward navigation using cursors.
