This is a part two of the Building Composable Commerce with Nuxt, Shopify, and Storyblok Crash Course.
Nuxt is an Intuitive Web Framework that allows you to build your next Vue.js application with confidence. An open source framework under MIT license that makes web development simple and powerful.
It comes with several useful features like:
- Optimized with code-splitting, tree-shaking, optimized cold-start, link prefetching, payload extraction, just to name a few. Fast by default so you can focus on building.
- Decide what rendering strategy at the route level: SSR, SSG, CSR, ISR, ESR, SWR. Build any kind of website or web application with optimized performance in mind.
- By leveraging server-side rendering, ESM format and optimized images, Nuxt websites are indexable by search engines while giving the feeling of an app to the end-users.
Read more about it in the official documentation → https://nuxt.com/
We will be using this framework for our storefront (the frontend of e-commerce application) that will connect later on to the Shopify platform by using the Apollo GraphQL and to Storyblok as CMS.
Getting started with Nuxt
In order to get started with Nuxt, the best place to go is the official documentation → https://nuxt.com/docs/getting-started/installation. We will create a simple Nuxt application with the following command in the terminal:
npx nuxi init nuxt-shopify-storyblok
This will create a simple Nuxt starter application. We can start this project to see if it works as expected (also, remember to install the dependencies with the package manager of your choice (in my case it was yarn
).
Running the project requires below command:
yarn dev
The project should be running right now and when we access the browser we should see the following result:
Now, if that went well, we can move to the next section about adding a bit of styling so that our future e-commerce application will look a bit better.
Adding styling with TailwindCSS
One of the things that I love about Nuxt is its ecosystem of modules. They extend the default functionality of the core framework and deliver a great Developer Experience.
Let’s add @nuxtjs/tailwindcss
module to our application so that we can have easy and flexible styling for our e-commerce website.
Install the module with the following command:
yarn add --dev @nuxtjs/tailwindcss
And now, let’s add it to the modules
array in nuxt.config.ts
file:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss']
})
And that’s it! We now have TailwindCSS installed in our application and we can test it out by replacing the code in app.vue
component to something like this:
class="text-xl text-green-500">Hello Nuxt with TailwindCSS
The basic layout of our e-commerce website
To have better reusability of our storefront application elements, let’s create a layout that will be shared across our pages. We will create two components, namely TheHeader
and TheFooter
and finally a layout that we will be using for our upcoming pages.
Let’s start with TheHeader
component that will be a simple navigation with just a logo of Nuxt (company logo) on the left side of the page. I have already added a logo.svg
in the /public/logo.svg
so that we could use it easily in our app.
// components/TheHeader.vue
As you can see here, it is a simple nav
element with NuxtLink
that when clicked will redirect the user to the homepage. Nothing crazy here 😀
Next, we will create TheFooter
component that will be responsible for displaying a footer
tag with two links; first to the Nuxt website, and second to your Twitter:
// components/TheFooter.vue
Nothing crazy here as well. Now, let’s create a default.vue
layout and add two previously created components there:
class="px-20">
/>
/>
/>
The content of our pages (home page and product page) will be rendered in
. Finally, let’s add this layout in our global app.vue
component:
name="default">
/>
The
will not work yet as we have not created any pages yet (also, the warning in the console will confirm that → Create a Vue component in the pages/ directory to enable
) but no worries, we will do that in the next section.
If we did everything correctly, we should see the following result in the browser:
We have a header component with the Nuxt logo and also a footer with a link to Nuxt and made by me link. In the next section, we will create the homepage of our e-commerce website.
Homepage with Banner and Product List
In this section, we will be creating two new components, namely HeroBanner.vue
and ProductCard.vue
as well as the new page. We won’t be fetching any data yet but instead we will mock the data for now. We will also add a new module @nuxt/image
that will be responsible for optimizing our images so that they are more performant out of the box.
To use the @nuxt/image
module in our Nuxt app we will use the official docs
In order to use it, we will first install it:
yarn add --dev @nuxt/image-edge
And then, add it to our modules
array in nuxt.config.ts
file:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@nuxt/image-edge']
})
And that’s it! We can now use NuxtImg
component in our app.
You may be wondering why we are using the image-edge
version. At the time of me writing this article, this is the recommended version of the image module to work with Nuxt 3
For our new components, let’s start with Hero Banner. As its name suggests it will be the first banner that our users will see so we want to make it big and with a catchy phrase at the top.
src="https://mdbootstrap.com/img/new/textures/full/142.jpg"
class="h-[500px] w-full"
format="webp"
/>
class="mx-auto px-32">
class="text-7xl font-bold text-center text-gray-800 rounded-lg shadow-lg py-16 px-12 bg-white/70 -mt-[170px] backdrop-blur-xl"
>
class="mb-3">Find the best products
class="text-green-600">on the market
The second part of this component is simple, we are just displaying a div with a green text, but let’s stop for a second to discuss what is happening in NuxtImg
component.
We are using the component from image module and we are passing a prop attribute format
. In here we are saying what format we would like this image to be. So, instead of heavy .jpg
we would prefer to have a lighter alternative of .webp
that is supported by all modern browsers. The optimization won’t work yet as we need to apply some configuration to the image module.
By default, the IPX image optimizer that the image module is using, needs to have a list of domains that can be used for optimizing the images. For now, what would happen is that our Banner would be displayed, but it wont be optimized. In order to enable the optimization, we need to add the [mdbootstrap.com](http://mdbootstrap.com)
to the allowed domains for IPX. Let’s do this below:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss', '@nuxt/image-edge'],
image: {
domains: ['mdbootstrap.com']
}
})
Let’s now create our homepage so that we could display the HeroBanner.vue
component there:
/>
Thanks to the Nuxt auto-import feature, we do not need to write any import statements.
If we did everything correctly, we should see the following result in the browser:
Now, let’s create a ProductCard.vue
component that will be responsible for displaying the data about our product (mocked for now).
// components/ProductCard.vue
<script setup lang="ts">
defineProps({
title: {
type: String,
required: true,
},
price: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
});
script>
<template>
<div class="mx-2">
<div class="relative rounded-lg shadow-lg">
<NuxtLink :to="link">
<NuxtImg
:src="image"
class="shadow-lg rounded-lg opacity-1 hover:opacity-75 transition duration-300 ease-in-out"
format="webp"
/>
NuxtLink>
<div class="p-6">
<h5 class="font-bold text-lg mb-3">{{ title }}h5>
<pre class="text-gray-500 mb-4">{{ price }}pre>
<p>{{ description }}p>
div>
div>
div>
template>
This component will accept a few props that will be used to display content about our product in a nice way.
Let’s add it to our index.vue
(Home Page) to see how it looks like. I will create an array of mocked products to display a list of three products below the HeroBanner.vue
component.
// pages/index.vue
<script setup lang="ts">
const mockedProducts = [
{
id: 0,
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80',
title: 'My awesome product 1',
price: '10$',
link: '#',
description: 'My awesome product 1'
},
{
id: 1,
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80',
title: 'My awesome product 2',
price: '15$',
link: '#',
description: 'My awesome product 2'
},
{
id: 2,
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80',
title: 'My awesome product 3',
price: '20$',
link: '#',
description: 'My awesome product 3'
}
]
script>
<template>
<div>
<HeroBanner />
<div class="flex my-20">
<ProductCard
v-for="{ id, image, title, price, link, description } in mockedProducts"
:key="id"
:image="image"
:title="title"
:price="price"
:link="link"
:description="description"
/>
div>
div>
template>
Nothing crazy here as well, we are just creating three components out of the array of mocked products. Later on, we will replace this array with actual data from the Shopify platform.
Product Detail Page
Apart from the Home Page, our e-commerce application would need a solid Product Page that would showcase our product, related products and would allow us to buy the actual product.
Let’s create a new page in /pages/products/[handle].vue
.
<script setup lang="ts">
const mockedProduct = {
image:
"https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80",
title: "My awesome product 1",
price: "10$",
description: "My awesome product 1",
};
script>
<template>
<section>
<div class="grid grid-cols-2 items-center px-20">
<NuxtImg
:src="mockedProduct.image"
class="rounded-lg shadow-lg -rotate-6"
alt="Product Image"
format="webp"
/>
<div class="rounded-lg shadow-lg p-12 backdrop-blur-2xl">
<h2 class="text-4xl font-bold mb-6">{{ mockedProduct.title }}h2>
<p class="text-gray-500 mb-6">
{{ mockedProduct.description }}
p>
<button
class="px-7 py-3 bg-green-600 text-white font-medium text-sm rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
>
Pay {{ mockedProduct.price }}
button>
div>
div>
section>
template>
The path in our project will resemble the URL in the browser so:
products/[handle].vue
→ http://localhost:3000/products/3
If we did everything correctly, we should see the following result in the browser:
Apart from that, let’s also add the same list of products from the HomePage (don’t bother about code duplication right now, everything will be fixed once we will fetch the real data from Shopify 😀)
<script setup lang="ts">
const mockedProduct = {
image:
"https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80",
title: "My awesome product 1",
price: "10$",
description: "My awesome product 1",
};
const mockedProducts = [
{
id: 0,
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80',
title: 'My awesome product 1',
price: '10$',
link: '#',
description: 'My awesome product 1'
},
{
id: 1,
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80',
title: 'My awesome product 2',
price: '15$',
link: '#',
description: 'My awesome product 2'
},
{
id: 2,
image: 'https://images.unsplash.com/photo-1505740420928-5e560c06d30e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8cHJvZHVjdHxlbnwwfHwwfHw%3D&w=1000&q=80',
title: 'My awesome product 3',
price: '20$',
link: '#',
description: 'My awesome product 3'
}
]
script>
<template>
<section>
<div class="grid grid-cols-2 items-center px-20">
<NuxtImg
:src="mockedProduct.image"
class="rounded-lg shadow-lg -rotate-6"
alt="Product Image"
format="webp"
/>
<div class="rounded-lg shadow-lg p-12 backdrop-blur-2xl">
<h2 class="text-4xl font-bold mb-6">{{ mockedProduct.title }}h2>
<p class="text-gray-500 mb-6">
{{ mockedProduct.description }}
p>
<button
class="px-7 py-3 bg-green-600 text-white font-medium text-sm rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
>
Pay {{ mockedProduct.price }}
button>
div>
div>
<div class="flex my-20">
<ProductCard
v-for="{ id, image, title, price, link, description } in mockedProducts"
:key="id"
:image="image"
:title="title"
:price="price"
:link="link"
:description="description"
/>
div>
section>
template>
Apart from the product detail, we also have a carousel below that shows a list of products (in the future those products will be related to the main product).
That was a lot of work so let’s take a short brake and let’s move to the next section where we will be replacing the mocked data, with real data from Shopify.