Skip to content

RESOURCES / BLOG

Using Shoppable Video in Next.js

YouTube, Twitch, and TikTok are some of the most used platforms to promote and advertise products. According to Blog Hubspot, consumers prefer videos to images because videos give a better overview of the product.

Shoppable Video is an e-commerce feature by Cloudinary that allows users to add “shoppable” items in their product videos. This feature will enable customers to shop while watching your videos. The customers can shop the items that are then added to the cart. They will be redirected to a page to make a purchase when done shopping.

In this article, we will learn how we can render Shoppable videos in a NextJS app.

You can learn more on Shoppable video from the Cloudinary documentation.

This Shoppable Video feature is included in Cloudinary’s video package. Therefore, we can use the Cloudinary package to add the Shoppable Video feature to our videos. We will also learn how to render data of any selected item in the shoppable video’s product bar.

To demonstrate this, we will build a Next.js web app. This app will render the product videos on an index page. We will have a product details page, and this page will open to display a product when it’s clicked on the product bar.

On the product details page, the product’s name, price, image are shown, and an Add to Cart button to add the product to our shopping basket.

Our final app will look like this:

The entire project was completed on CodeSandbox. Fork it to run the code.

    

GitHub URL: https://github.com/nextjs-prj/cloudinary-shoppable

## Prerequisites & Installation

The knowledge of JavaScript and React.js is required for this project. We require a Node.js binary and NPM, its package manager installed.  

To create a new Next.js app, we open the terminal in our desired directory and run the below command:

```bash
    yarn create next-app nextjs-shop
    # OR
    npx create-next-app nextjs-shop

Alternatively, you can create a Next.js project using yarn, as seen above.

This command will create a Next app in the nextjs-shop directory. We move into the directory with the command cd nextjs-shop.

The Shoppable Video feature was built by Cloudinary; therefore, to add the feature to our app, we have to install Cloudinary’s SDK and its dependencies, which include the following:

  • lodash : Contains major utility functions for Nodejs.
  • cloudinary-core: Cloudinary JavaScript library that provides Cloudinary’ APIs and administration capabilities to web development.
  • cloudinary-video-player: The Cloudinary Video Player is a JavaScript-based HTML5 video player bundled with many valuable customization and integration capabilities and is monetization and analytics-ready.

We will reference the lodash library, Cloudinary core and the Cloudinary video player directly from the Unpkg CDN.

In the root directory of the project, open _app.js and update the returned JSX elements in the <Head>..</Head> section to include:

      return (
        <>
          <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
            <link
              href="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.css"
              rel="stylesheet"
            />
            <script
              src="https://unpkg.com/cloudinary-core@latest/cloudinary-core-shrinkwrap.min.js"
              type="text/javascript"
            ></script>
            <script
              src="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.js"
              type="text/javascript"
            ></script>
          </Head>
    //...
Code language: JavaScript (javascript)

This will load all the libraries once the app starts, and cloudinary is available to all components through the window object.

We will create our page route files. Then, the already existing index.js in the pages folder will render the index page.

So we create the pages using the terminal command in the project’s pages directory:

    mkdir product
    touch product/[id].js
    
    mkdir cart
    touch cart/index.js

The product/[id].js is a dynamic page. It takes the id of each component in its [id]. The cart/index.js is the page that lists the cart.

In _app.js, we will set up a cart provider using React’s Context API. Then, we will persist products added to our cart using localStorage. We will have methods getCart, removeFromCart and addToCart. The Context Provider will pass around these methods, and components will consume them via the useContext hook. To do this, first, we import all required dependencies using:

    import "../styles/globals.css";
    import Header from "../components/Header";
    import Head from "next/head";
    import React, { useCallback } from "react";
    
    export const CartContext = React.createContext(null);
Code language: JavaScript (javascript)

Then, we create a MyApp component with functions to get, add, and remove an item from cart:

    function MyApp({ Component, pageProps }) {
      const getCart = useCallback(() => {
        const state = window.localStorage.getItem("state");
        if (state) {
          return JSON.parse(state);
        }
        return [];
      });
    
      const addToCart = useCallback((product) => {
        const state = window.localStorage.getItem("state");
        if (state) {
          const stateObj = JSON.parse(state);
          stateObj.push(product);
          window.localStorage.setItem("state", JSON.stringify(stateObj));
        } else {
          const stateObj = [product];
          window.localStorage.setItem("state", JSON.stringify(stateObj));
        }
        window.location.reload();
      });
    
      const removeFromCart = useCallback((product) => {
        const state = window.localStorage.getItem("state");
        const stateObj = JSON.parse(state);
        const remProducts = stateObj.filter((_prduct) => _prduct.id != product?.id);
        window.localStorage.setItem("state", JSON.stringify(remProducts));
        window.location.reload();
      });
    
      return (
        <>
          
        </>
      );
    }
    
    export default MyApp;
Code language: JavaScript (javascript)

We created a CartContext by calling the React.createContext API. Finally, we return the JSX component:

    function MyApp({ Component, pageProps }) {
      // [component methods go in here]
      return (
        <>
          <Head>
            <title>Create Next App</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
            <link
              href="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.css"
              rel="stylesheet"
            />
            <script
              src="https://unpkg.com/cloudinary-core@latest/cloudinary-core-shrinkwrap.min.js"
              type="text/javascript"
            ></script>
            <script
              src="https://unpkg.com/cloudinary-video-player@1.5.3/dist/cld-video-player.min.js"
              type="text/javascript"
            ></script>
          </Head>
    
          <Header />
          <CartContext.Provider value={{ getCart, addToCart, removeFromCart }}>
            <Component {...pageProps} />
          </CartContext.Provider>
        </>
      );
    }
    
    export default MyApp;
Code language: JavaScript (javascript)

Here the Header component is rendered before the Component. This was done so that the Header will appear on all pages.

Component is the component being rendered in a route. We enclosed the Component with the CartContext.Provider passing in an object to its value attribute. The object contains the methods getCart, removeFromCart and addToCart.

We create a file to contain a Header component using the terminal command:

    mkdir components components/Header
    touch components/Header/index.js touch components/Header/Header.module.css

We updated the content in components/Header/index.js to:

    import { header, headerName } from "./Header.module.css";
    
    export default function Header() {
      return (
        <section className={header}>
          <div className={headerName}>Shopify</div>
        </section>
      );
    }
Code language: JavaScript (javascript)

It renders “Shopify”, the name of our app.

We will proceed to create a Cloudinary instance and render our product videos. In pages/index.js, we first import all dependencies with:

    import Head from "next/head";
    import styles from "../styles/Home.module.css";
    import Header from "../components/Header";
    import { ProductCard } from "../components/ProductCard";
    import { useContext, useEffect, useState } from "react";
    import { CartContext } from "./_app";
    import Link from "next/link";

Next, we created a Home component with:

```jsx
    export default function Home() {
      const [cld, setCld] = useState();
      const [cart, setCart] = useState([]);
      const { getCart } = useContext(CartContext);
    
      useEffect(() => {
        if (cloudinary) {
          const _cld = cloudinary.Cloudinary.new({ cloud_name: "demo" });
          setCld(_cld);
          setCart(getCart());
        }
      }, []);
    
      return (
        <div className={styles.container}>
          <Head>
            <title>Shopify</title>
            <meta name="description" content="Generated by create next app" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
    
          <main className={styles.main}>
            <div className={styles.breadcrumb}>
              <h2>Shopify Products</h2>
              <span></span>
            </div>
    
            <div className={styles.productscontainer}>
              <div className={styles.yourproducts}>
                <h3>Product Videos</h3>
                <span>
                  <Link href="/cart">
                    <span
                      style={{
                        cursor: "pointer",
                        textDecoration: "underline",
                        color: "blue",
                      }}
                    >
                      Cart: <span>{cart?.length}</span>
                    </span>
                  </Link>
                </span>
              </div>
              <div>
                {cld &&
                  ["rafting", "sea_turtle", "forest_bike"].map((product, i) => (
                    <ProductCard cld={cld} product={product} key={i} />
                  ))}
              </div>
            </div>
          </main>
        </div>
      );
    }
Code language: HTML, XML (xml)

In the useEffect hook, we got the cloudinary object and created a Cloudinary instance.

The cloud_name passed to the Cloudinary.new(..) call is a unique indentifier for our Cloudinary account.

To obtain a cloud name, create a Cloudinary account for free. The cloud name is visible on your Cloudinary dashboard.

_cld is an instance of the Cloudinary class. With this, we can call different methods and properties on the Cloudinary object. We added the product videos name in an array and rendered the ProductCard component for each. The ProductCard is passed the cld instance and the product video name via similarly named props.

Before we create the ProductCard component, we have to set up the shoppable configuration.

We have product videos forest_bike, sea_turtle and rafting. We will set a shoppable configuration in the data directory of the project that will be passed to the Cloudinary player when initializing the videos. Here is the configuration for the rafting product video in this GitHub Gist:

https://gist.github.com/Chuloo/5db54cf58a83f2dd4c65bd7a134817a1

The products array in the shoppable object holds the shoppable config for all the products in the rafting product video.

This snippet sets the raft in the video as a product in the products bar. Here’s a description of each key:

  • The productId is the ID of the product.
  • The productName is the name of the product. It is set to Raft.
  • The startTime is the time the raft appears in the video. The unit is in milliseconds.
  • The endTime is the time the raft product no longer appears in the video.
  • The publicId is the ID of the product image. The ID can be the id of the image in our Cloudinary account, or the id can be a public image URL.
  • The hotspots hold places in the video where we can add hotspots. The viewers can click on them for more info on the product.

Let’s see the object in the hotspots array:

  • time: the exact time to display the hotspot.
  • x: The position of the hotspot on the x-axis.
  • y: The position of the hotspot on the y-axis.
  • tooltipPosition: This is the alignment of the hotspot. The text can be aligned from either the left or from the right.
  • Finally, clickUrl: This is the URL to visit when the URL is clicked.
  • The onClick sets the click-behaviour of the product in the products bar.
  • The action is the action to perform when clicked. It is set to goto, which will load a URL in a separate tab.
  • The args object sets the argument passed to the goto action.
  • The url sets the URL to go to when the product is clicked. Here, we put it to the dynamic route product/[id] with the id being raft.

Similarly, we have Shoppable configuration objects for both the sea_turtle and forest_bike product videos.

This component renders each product video. We create new files for the component and its CSS modules using the terminal command:

    mkdir components/ProductCard
    touch components/ProductCard/index.js 
    touch components/ProductCard/ProductCard.module.css

Next, we edit ProductCard/index.js to:

    import { useEffect, useRef } from "react";
    import styles from "./ProductCard.module.css";
    import data from "./../../data";
    
    export function ProductCard({ cld, product }) {
      const videoRef = useRef(null);
      useEffect(() => {
        const source = data[product];
        const demoplayer = cld.videoPlayer(videoRef.current).width(850);
        demoplayer.source(product, source);
      });
    
      return (
        <div className={styles.productcard}>
          <video width="500" ref={videoRef} controls muted preload="false">
            {" "}
          </video>
        </div>
      );
    }
Code language: JavaScript (javascript)

Here we destructured cld and product from the props passed which are the Cloudinary instance, and the name of the Cloudinary’s video to render, respectively.

In the useEffect hook we set the shoppable config from the product passed. Then we called the videoPlayer() method from the cld Cloudinary instance. This videoPlayer() method is passed the reference to a video element. This action makes Cloudinary sets its features on the video HTMLElement. We passed videoRef.current to the method. This holds a reference to the video element in our UI. The width method sets the width of the video element. The third line of code calls the source method in the demoPlayer. This demoPlayer holds a reference to the video element. We passed the product to the source method as the first argument and the source as the second argument. The source has the shoppable configuration of each video.

With this, we have configured and rendered shoppable videos in a Nextjs app.

Next, we’ll scaffold other components.

This component will render each product card in the cart. We create the component file and CSS module using the terminal command:

    mkdir components/CartCard
    touch components/CartCard/index.js touch components/CartCard/CartCard.module.css

We update CartCard/index.js to:

    import styles from "./CartCard.module.css";
    
    export default function CartCard({ cart }) {
      const { name, imageUrl, price } = cart;
      return (
        <div className={styles.cartcard}>
          <div
            style={{ backgroundImage: `url(${imageUrl})` }}
            className={styles.cartcardimg}
          ></div>
          <div className={styles.cartcarddetails}>
            <div className={styles.cartcardname}>
              <h3>{name}</h3>
            </div>
            <div className={styles.cartcardprice}>
              <span>{price}</span>
            </div>
          </div>
        </div>
      );
    }
Code language: JavaScript (javascript)

Here, we destructured the cart object from the component props. Then, we render the name, the price and the image URL of the product.

We already configured each product in the product bars of each video to load the route product/[id]. So the id is the name of the product. For example, product/raft, id becomes raft. In data/product_data.js we create product data that will hold the price, name, and the image URL of each shoppable product.

You can find it in this GitHub Gist below.

https://gist.github.com/Chuloo/3809aae48de401ae193ea1c5f47904d4

https://gist.github.com/Chuloo/3809aae48de401ae193ea1c5f47904d

Each object in the data array has an id to hold the unique ID of the product, name to hold the name of the product, imageUrl to hold the image URL of the product, and the price to hold the price of the product. In the product/[id].js page component, we will extract the product’s id from the id param, use it to get the product details from the data above, then render it.

First, we imported all required dependencies, defined the component, and its methods:

    import styles from "../../styles/ProductView.module.css";
    import { useRouter } from "next/router";
    import products_data from "./../../data/products_data";
    import { useEffect, useState, useContext } from "react";
    import { CartContext } from "./../_app";
    
    export default function Product(params) {
      const { getCart, removeFromCart, addToCart } = useContext(CartContext);
      const [inCart, setInCart] = useState(false);
    
      const router = useRouter();
      const {
        query: { id },
      } = router;
      const product = products_data.find((_product) => _product.id == id);
    
      useEffect(() => {
        const cart = getCart();
        const _cartFound = cart.find((_prdct) => _prdct.id == id);
        const inCart = _cartFound ? true : false;
        setInCart(inCart);
      });
    
      function _addToCart() {
        addToCart(product);
      }
    
      function _removeFromCart() {
        removeFromCart(product);
      }
    
      return (
        
    }
Code language: JavaScript (javascript)

We got the value of the CartContext using the useContext hook. The state inCart is a boolean to hold a true value if the product is already in the shopping cart, and false if otherwise. Next, we extracted the id of the product from the params using the useRouter hook. Finally, we get the details of the product from the data object and then render it on the UI. The useEffect callback checks the cart to know if the product has been in the cart and then sets the inCart state appropriately.

The _addToCart and _removeFromCart functions all call the addToCart and removeFromCart functions in the CartContext.

Next we render the JSX elements with:

    export default function Product(params) {
    // component definitions go in here
      return (
        <div className={styles.productviewcontainer}>
          <div className={styles.productviewmain}>
            <div
              style={{ backgroundImage: `url(${product?.imageUrl})` }}
              className={styles.productviewimg}
            ></div>
            <div style={{ width: "100%", marginLeft: "15px" }}>
              <div className={styles.productviewname}>
                <h1>{product?.name}</h1>
              </div>
              <div className={styles.productviewminidet}>
                <div
                  style={{
                    borderTop: "1px solid black",
                    borderBottom: "1px solid black",
                    paddingTop: "18px",
                    paddingBottom: "18px",
                  }}
                >
                  <span
                    style={{
                      marginRight: "4px",
                      color: "rgb(142 142 142)",
                      display: "flex",
                      alignItems: "center",
                    }}
                  >
                    Price:{"  "}
                    <span style={{ color: "black", fontSize: "2em" }}>
                      {product?.price}
                    </span>
                  </span>
                </div>
                <div style={{ padding: "14px 0" }}>
                  <span>
                    {!inCart ? (
                      <button className="btn" onClick={_addToCart}>
                        Add to Cart
                      </button>
                    ) : (
                      <button className="btn-danger" onClick={_removeFromCart}>
                        Remove from Cart
                      </button>
                    )}
                  </span>
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }
Code language: JavaScript (javascript)

We used the inCart value to conditionally render either an Add to Cart or Remove from Cart button.

This page will render all the products in our cart. In a new index.js file in the pages/cart directory, we add the cart page component with:

    import styles from "./../../styles/Home.module.css";
    import { CartContext } from "./../_app";
    import { useContext, useEffect, useState } from "react";
    import CartCard from "./../../components/CartCard";
    
    export default function Cart() {
      const { getCart } = useContext(CartContext);
      const [cart, setCart] = useState([]);
    
      useEffect(() => {
        const cart = getCart();
        setCart(cart);
      });
    
      return (
        <div className={styles.container}>
          <main className={styles.main}>
            <div className={styles.breadcrumb}>
              <h2>Cart</h2>
              <span>
                <button
                  className="btn"
                  style={{ backgroundColor: "green", borderColor: "green" }}
                >
                  Checkout
                </button>
              </span>
            </div>
    
            <div className={styles.productscontainer}>
              <div>
                {cart.map((_cartItem, i) => (
                  <CartCard cart={_cartItem} key={i} />
                ))}
              </div>
            </div>
          </main>
        </div>
      );
    }
Code language: JavaScript (javascript)

Here, we got the getCart function from the CartContext using the useContext hook. The cart state is an array that holds the products in our cart. In the useEffect hook, the getCart function gets called, and the result is added to the cart state. The cart is then rendered using the map function. Finally, each cart item is rendered with the CartCard component.

Shoppable Videos add interactivity to your product videos. This increases purchases and store conversion. In this tutorial, we started by introducing and learning about Shoppable videos. We demonstrated how to render them in a Nextjs app. Finally, we built a live Next.js store to demo the usage.

Head over to the Shoppable Video docs to learn more. You can find the source code of the project here.

You may find the following resources useful.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free