Our Writing

Building a JAMstack shop with Strapi 4, Nuxt 3, Snipcart - part 3.

Gemma

Gemma /

This is part three in our series on how to create a JAMStack e-commerce site with Nuxt 3, Strapi 4 and Snipcart. In this post, we will pull through the products from Strapi with Nuxt and style them to match the designs.

If you’re still here, then woweeeee, you do have patience hehe. If you’ve stumbled across this post and would like to know more about this flaming hot candle shop then look no further:

So I know Christmas has come and gone, and your shop wasn’t quite ready for Christmas but don’t let that disappoint you, let’s get fired up again, and light up your January by finishing the last few steps of your brand new shop. You can show it off to all your friends and get them to buy lots of flaming hot new products.

If you are the sort of person that prefers to watch their candle shops being made rather than reading about them being made, here's part 3 in video form. If not follow along below.

You can get the code for the finished front-end and backend below.

We’re going to start by building out a couple of new components:

ProductTeaser.vue so we can loop over the products coming back from our Strapi backend and they’ll all have exactly the same styling. The product teaser will look like this:

The product teaser component

ProductImage.vue will display the product image. It will ensure they are always square and will compute the URL for the image from the data received back from Strapi. So let’s go ahead and make these inside the components directory.

components/ProductImage.vue
HTML
<template>
  <div class="bg-gray-100 shadow-xl aspect-w-1 aspect-h-1">
    <img :src="imageUrl" :alt="product.attributes.Title" loading="lazy" />
  </div>
</template>

<script setup>
const props = defineProps({
  product: Object
});

const config = useRuntimeConfig();
const imageUrl = computed(() => {
  return `${config.API_URL}${props.product.attributes.Image.data.attributes.formats.medium.url}`
});
</script>

This is a pretty simple component that takes the product as a prop and outputs its image within a div.

We use the Tailwind aspect ratio helpers to ensure the result is always square.

We also use a computed property to generate the correct URL for the image. The URL provided by Strapi is relative so we need to make it absolute by adding the API url defined in our env to the front.

components/ProductTeaser.vue
HTML
<template>
  <article>
    <ProductImage
      :product="product"
      class="mb-6 transition-transform transform group-hover:scale-105"
    />
    <Heading tag="h3" font-style="h4">{{ product.attributes.Title }}</Heading>
    <p class="text-lg text-brand-grey-400">£{{ product.attributes.Price }}</p>
  </article>
</template>

<script setup>
const props = defineProps({
  product: Object
});
</script>

The product teaser component takes the product as a prop and uses our new ProductImage component and outputs the product image as well as displaying the product name and price. This will be the component we use to display products whenever they appear in a product list.

We now need to create a single product page template. It will look like this:

Our basic single page template

It will also have some related products on the page as well like so:

The related products section

If you create a new directory called product inside the pages directory and create a file called [id].vue .

pages/products/[id].vue
HTML
<template>
  <div>
    <Container class="grid grid-cols-12 gap-2 pt-32 pb-24 md:gap-12">
      <div class="col-span-12 md:col-span-6 lg:col-span-5">
        <ProductImage :product="product.data" />
      </div>
      <div class="col-span-12 md:col-span-6 lg:col-span-7">
        <Heading tag="h2" font-style="h1">{{ product.data.attributes.Title }}</Heading>
        <p class="mb-6 text-2xl text-brand-grey-600">£{{ product.data.attributes.Price }}</p>
        <p class="pr-12 mb-6 text-brand-grey-400">{{ product.data.attributes.Description }}</p>

        <div class="flex items-center">
          <input-field type="number" class="mr-4" min="1" v-model="quantity" />
          <Btn

            @update:modelValue="pageTitle = $event"
          >Add to basket</Btn>
        </div>
      </div>
    </Container>
    <Container>
      <div class="pb-8">
        <Heading tag="h3" font-style="h3">Related products</Heading>
        <Heading tag="h2" font-style="h2">Other sick wicks</Heading>
      </div>
      <div class="grid grid-cols-2 gap-12 md:grid-cols-4">
        <product-teaser
          class="col-span-1"
          v-for="product in products.data"
          :key="product.id"
          :product="product"
        />
      </div>
      <div class="flex justify-center pt-12 pb-32">
        <Btn theme="secondary">View the other sick wicks</Btn>
      </div>
    </Container>
  </div>
</template>

<script setup>
const route = useRoute();
const config = useRuntimeConfig();
const quantity = ref(1);
const { data: products } = await useFetch(`${config.API_URL}/api/products?populate=*`)
const { data: product } = await useFetch(`${config.API_URL}/api/products/${route.params.id}?populate=*`)
const imageUrl = computed(() => {
  if (!product.value.data) {
    return null;
  }

  return `${config.API_URL}${product.value.data.attributes.Image.data.attributes.url}`
});
</script>

Dynamic routes with Nuxt

By creating the single product page at pages/products/[id].vue it allows us to use Nuxt’s dynamic routing to create pages for each product in our shop.

In our component we can get the current product id from the route params. We can then use this to adjust our query to Strapi so we load a single product.

To do this we first get a reference to the current route:

pages/products/[id].vue
JAVASCRIPT
const route = useRoute();

Next we use the id route parameter to construct our request to Strapi in Nuxt’s useFetch hook.

pages/products/[id].vue
JAVASCRIPT
const { data: product } = await useFetch(`${config.API_URL}/api/products/${route.params.id}?populate=*`)

👉 Notice the ?populate=* query param. This tells Strapi to load all of the model's relations. Without this, the image fields would be empty and we wouldn’t be able to get the URLs for the images.

Once we have the product data, it's just a case of using it in the template to display your candle.

Creating a Vue 3 input field

You might have noticed there is one more missing component on the product page above. An input field. This will be used to allow the user to adjust the quantity of the product.

components/InputField.vue
HTML
<template>
  <input
    class="w-24 px-4 py-4 text-xl bg-black rounded-none outline-none bg-opacity-10 placeholder-brand-grey-600 text-brand-grey-600"
    :placeholder="placeholder"
    :type="type"
    :value="modelValue"
    @change="$emit('update:modelValue', $event.target.value)"
    name="text"
  />
</template>


<script setup>
const props = defineProps({
  type: String,
  placeholder: String,
  modelValue: [String, Number]
});
</script>

<style scoped>
</style>

This is just a basic input with a few prop bindings. The most interesting thing here is the use of the modelValue prop. This will allow us to use this component using the v-model syntax. Notice the @change handler. This will update the parent’s v-model whenever the input changes.

Finally, now we have a single product page, we need to add a link around the product teaser so the user an actually get there!

components/ProductTeaser.vue
HTML
<template>
  <article>
    <nuxt-link :to="`/products/${product.id}`" class="group">
      <ProductImage
        :product="product"
        class="mb-6 transition-transform transform shadow-xl group-hover:scale-105"
      />
      <Heading tag="h3" font-style="h4">{{ product.attributes.Title }}</Heading>
      <p class="text-lg text-brand-grey-400">£{{ product.attributes.Price }}</p>
    </nuxt-link>
  </article>
</template>

<script setup>
const props = defineProps({
  product: Object
});
</script>

That's it for the time being. Hopefully, you enjoyed seeing your candle shop come together? As always if you have any questions please reach out to us: @pixelhopio.

In the next post, we will be adding the final magic to turn out candle shop into a fully functional e-commerce store when we integrate Snipcart. If you don't want to miss out on part 4, be sure to subscribe to our newsletter, subscribe on youtube, or follow us on Twitter!

4 Responses

Want to respond? Tweet this post!

4 Likes

Subscribe.

Like what you’re reading? Sign up to recieve our updates straight to your inbox. No spam, unsubscribe anytime!

Related posts.

Creating an internationalised site with Strapi and Nuxt

We were really excited when Strapi released a new update a few weeks ago that included internationalisation! This post will show you the basics of creating an internationalised site using Strapi and Nuxt.

Read more

Dynamic social images with Nuxt, Cloud Functions and Cloudinary

Every little helps when trying to get your content noticed on social media, and having an eye-catching sharing image could be the difference between getting someone passing you by or someone clicking through. This post will guide you by creating dynamic social sharing images unique to each page/post using Firebase cloud functions, Cloudinary and Nuxt.

Read more