Examples

Collection Page

How to create a collection page for your Shopify store in Nuxt
With the upcoming release of @shopify/hydrogen-vue the examples will be updated to use the new composables and components, so the current examples may become outdated.

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

Accessoires

Finishing touches that carry the look — sunglasses, sneakers, bags and slides built for everyday wear.

Beanie
This beanie is perfect for all seasons! Crafted in luxurious cashmere – it's classic, warm and comfortable. Just the thing to keep you stylish and cozy.
from $100.00
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 $180.00
Clear Sunnies
These modern clear sunglasses provide 100% UV400 protection from harmful sunrays and feature mirrored lenses for a timeless and stylish look. With lightweight construction and comfortable fit, you can look cool and stay safe in any situation.
from $25.00
Gray Runners
These gray runners are the perfect choice for running enthusiasts. These shoes provide superior breathability and comfort, so you can run longer with less fatigue. The lightweight design and airy mesh material make these shoes durable and lightweight, giving you the support you need for peak performance.
from $30.00

In order to follow this example, 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
    }
  }
`

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
      }
    }
  }
`, {
  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">
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
        provider="shopify"
        :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
      }
    }
  }
`, {
  variables: computed(() => ({
    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.

Published under the MIT License