Recipes

Collection Page

How to create a collection page for your Shopify store in Nuxt

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

High Top Sneakers
These stylish and durable high top sneakers are perfect for any casual look, offering superior comfort and protection with their foam cushioning and reinforced heel support.
from CA$180.00
Canvas Sneakers
These high-quality canvas sneakers offer a comfortable fit and superior breathability, thanks to their cushioning midsoles and durable construction. An array of stylish colors adds to the appeal, making them perfect for casual wear. Slip them on and enjoy reliable performance and style that lasts.
from CA$40.00
White Leather Sneakers
from CA$90.00
Frontpack
This frontpack is the perfect combination of form and function, with a modern, sporty design and patented technology that enables you to easily carry numerous items on the go. It's light, comfortable and has adjustable straps to fit all body types. Plus, its water-resistant outer shell ensures your items stay dry and secure.
from CA$200.00

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:

graphql/collection.ts
export const COLLECTION_FRAGMENT = `#graphql
  fragment CollectionFields on Collection {
    id
    title
    description
  }
`

The product fragment fetches the fields we need for each product card:

graphql/product.ts
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:

graphql/product.ts
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:

app/pages/collections/[handle].vue
<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:

app/pages/collections/[handle].vue
<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:

app/components/ProductCard.vue
<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:

app/pages/collections/[handle].vue
<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) and after (the end cursor of the current page).
  • When navigating backward, we pass last (items per page) and before (the start cursor of the current page).

In the template, we use the pageInfo to conditionally render previous and next buttons:

app/pages/collections/[handle].vue
<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.

Copyright © 2026