Ecommerce Sites with Cloudinary Widgets & Netlify

Banner for a MediaJam post

Ekene Eze

The power of compelling images increases the likelihood that a potential customer will make a purchase. When interactive, images provide a more profound experience for the customer, making them almost as tangible as the product themselves. Using Sanity, Cloudinary and Netlify I will show you how to set up your own interactive product media gallery with minimal effort.

In this tutorial, we will build the product Gallery management feature for an e-commerce web application using the Cloudinary Product Widget.

Sandbox

The completed project is on CodeSandbox. Fork it and run with it.

Prerequisites

Knowledge of React, Next.js, Sanity CLI, Netlify edge functions and a Cloudinary account is required to get the most out of this article.

Getting Started

Create a Next.js project by running the command below in your terminal.

1npx create-next-app product-gallery

Next, add the project dependencies using the following command:

1npm install axios @sanity/client @sanity/image-url netlify-cli dotenv -g

Next, navigate into the project directory.

1cd product-gallery

Then, run the command below to start the application.

1ntl dev

This should start our Next.js application on port 8888.

Note: You need to have the Netlify CLI installed to run ntl commands.

Reading data from Sanity

Our product data will be coming from a Sanity CMS project we’ve previously created. You can pull in data for products from wherever you want, a REST endpoint, your database, or from a JSON file. Whatever works best for you.

Let’s initialize a new Sanity instance for our project. Create a /lib/sanity.js file and update it with the snippet below:

1// sanity.js
2import sanityClient from "@sanity/client";
3export const client = sanityClient({
4 projectId: "[Project ID]",
5 dataset: "production",
6 useCdn: true,
7});

Here, we setup a new sanity client by passing it our projectId and dataset. We have now established a connection with Sanity to read data.

Fetching Products from Sanity

We are going to use Netlify functions to interface with our Sanity instance and fetch our products. To do that, let’s setup a simple Netlify function to fetch our data. Create a functions/product.js file from the root directory and update it with this snippet:

1// src/functions/product.js
2
3const sanityClient = require("@sanity/client");
4require("dotenv").config();
5
6exports.handler = async (event) => {
7 const client = sanityClient({
8 projectId: process.env.PROJECTID,
9 dataset: "process.env.DATASET,
10 useCdn: true,
11 token: process.env.TOKEN,
12 apiVersion: "2022-08-10"
13 });
14 try {
15 const products = await client.fetch(
16 `*[_type == "product" && "chocolate" in tags]`
17 );
18 const res = await products;
19 return {
20 statusCode: 200,
21 body: JSON.stringify(res)
22 };
23 } catch (error) {
24 console.log(error);
25 }
26};

Here, we used a GROQ query to fetch all products from Sanity with a chocolate tag and send it back as a response. As a result, if you visited our project at localhost:8888/.netlify/functions/product you should get a response containing all our products from Sanity.

Now that we are successfully fetching our product data from Sanity, it only makes sense that we display them on the homepage for users to see. To do that, let’s update our pages/index.js file to call our Netlify function and display the response (our products):

1// index.js
2import imageUrlBuilder from "@sanity/image-url";
3const builder = imageUrlBuilder(client);
4export default function IndexPage({ products }) {
5 return (
6 <div className={styles.container}>
7 <main className={styles.main}>
8 <div className={styles.grid}>
9 {products.map((product) => {
10 const { defaultProductVariant = {} } = product;
11 const { images } = defaultProductVariant;
12 return (
13 <div key={product._id}>
14 <a href={`/product/${product._id}`}>
15 <img
16 className={styles.card}
17 src={builder.image(images[0]).width(300)}
18 alt="images"
19 />
20 </a>
21 <h3>{product.title}</h3>
22 </div>
23 );
24 })}
25 </div>
26 </main>
27 </div>
28 );
29}
30export async function getStaticProps() {
31 const products = await axios.get(
32 "http://localhost:8888/.netlify/functions/products"
33 );
34 return {
35 props: {
36 products: products.data,
37 },
38 };
39}

With that, if we reload the homepage on the browser, we should now see our products from Sanity rendered for the users to see.

Adding Dynamic Product Details Pages

Now that we have our products displayed, we want to implement dynamic pages so that when you click on a product, we open a product details page where you can see more information about the product.

We’ll follow the standard Next.js approach of using getStaticPaths to generate the dynamic paths and build the pages on demand with our data. We’ll use the Cloudinary Product Gallery widget to display more images of the products. Create a p``ages/p``roduct/[id].js file and add the following snippet:

1// product/[id].js
2import axios from "axios";
3export default function product() {
4 return (
5 // Cloudinary product widget goes here
6 )
7}
8export default product;
9export async function getStaticPaths() {
10 const products = await axios.get(
11 "http://localhost:8888/.netlify/functions/products"
12 );
13 const paths = products.data && products.data.map((prod) => ({
14 params: { id: prod._id }
15 }));
16 return {
17 paths: paths,
18 fallback: false
19 };
20}

Here, we export the getStaticPath``() function to make requests to our Netlify function and generate all our products via the paths we’ve specified. Then, we loop through the products and assigned id to params.id to match the product ID.

Next, update the file and pass the paths we want to generate to the getStaticProps() function and pass the generated product to our homepage:

1// product/[id].js
2// ...
3export async function getStaticProps({ params }) {
4 const products = await axios.get(
5 "http://localhost:8888/.netlify/functions/products"
6 );
7 const product =
8 products.data && products.data.filter((prod) => params.id === prod._id);
9 return {
10 props: {
11 product,
12 },
13 };
14}

With this, we can pass the generated product to our IndexPage() component for rendering. However, we don’t just want to render it. We want to use the Cloudinary Product Gallery Widget to display all the different images of the product.

Setting up Cloudinary Product Gallery Widget

The Cloudinary gallery widget relies on the Client-side asset lists feature to retrieve the list of images (or videos) with specified tags. To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted. To enable it, open the Security settings in your console and clear the Resource lis**t item under Restricted media types**.

User-uploaded image: resource-list.png

Add Gallery Context

The functionality for the gallery widget is delivered via CDN and instantiated in the index.js file. However, we need access to the instantiated object elsewhere in our application to update the Widget on successful upload.

To avoid prop drilling, we’ll use React Context to pass this object to any component where it may be required. Create a /context/GalleryContext.js file in the root directory and set it up like so:

1import { createContext } from "react";
2const GalleryContext = createContext(null);
3export default GalleryContext;

With our context in place, we can instantiate the widget and update the context value.

Instantiate Gallery Widget

To instantiate the gallery widget we will use Next’s Custom Document to update the <html> tags used to render a Page.

To override the default Document, create the file pages/_document.js as shown below:

1import Document, { Html, Head, Main, NextScript } from "next/document";
2
3class MyDocument extends Document {
4 render() {
5 return (
6 <Html>
7 <Head />
8 <body>
9 <script
10 src="https://product-gallery.cloudinary.com/all.js"
11 type="text/javascript"
12 strategy="beforeInteractive"
13 ></script>
14 <Main />
15 <NextScript />
16 </body>
17 </Html>
18 );
19 }
20}
21
22export default MyDocument;

The snippet above is the default Document added by Next.js that allows custom attributes as props. We’re using it to embed our JavaScript file which incorporates all the product gallery functionality.

Next, update your products/[id].js file to match the following snippet:

1// product/[id].js
2// ...
3
4const product = ({ product }) => {
5 useEffect(() => {
6 const productGallery = cloudinary.galleryWidget(
7 {
8 container: "#gallery",
9 cloudName: "cloudName",
10 mediaAssets: [{ tag: `${product[0].tags[0]}`, mediaType: "image" }],
11 },
12 []
13 );
14 productGallery.render();
15 });
16 return (
17 <div className={styles.wrapper}>
18 <div>
19 <div id="gallery"></div>
20 </div>
21 <div>
22 <div>
23 <p className={styles.product}>{product[0].title}</p>
24 <p>{product[0].description}</p>
25 </div>
26 <p className={styles.price}>$ 20 </p>
27 <button className={styles.btn}>Add to Cart</button>
28 </div>
29 </div>
30 );
31};
32export default product;
33
34// ...

Here, we initialize the Product Gallery Widget with the cloudinary.galleryWidge``t() method call and pass the initialization options, including the cloud name and the tags of the media assets we wish to retrieve. We also specify a key named container to be the div element where the gallery will be rendered.

With these, we can now view the individual product and its variants. The final result will look like the GIF shown below.

Conclusion

In this tutorial, we went over the process of reading e-commerce product data from Sanity using Netlify functions and rendering multiple product variant images in a product display page using the Cloudinary Product Gallery Widget. We rendered only images in this project, however, the widget can also render videos, 360 spin sets and 3D models.

Resources you may find helpful:

Ekene Eze

Director of DX at Plasmic

I work at Plasmic as the Director of Developer Experience and I love executing projects that help other software engineers. Passionate about sharing with the community, I often write, present workshops, create courses, and speak at conferences about web development concepts and best practices.