Displaying products (CSR)

This is for:

Developer

Regardless of the product discovery solution you’re building (search, listings, or recommendations), displaying products to the user is an essential part of the commerce experience.

Products can be displayed either as part of a list of products or as a single product on a product detail page (PDP).

This article explains how to display products and ensure that the correct events, such as clicks and product views, are logged when a user interacts with a product.

Displaying lists of products

In your commerce application, you’ll often need to display a list of products, for instance, when showing search results, product recommendations, or product listing pages. It’s important to log click events when a user interacts with a product in these lists.

Accessing products in the controller

To interact with products and display them, use the controller specific to the product discovery solution you’re implementing.

Regardless of the controller you use, you can display products by accessing the state.products object on the controller. This object contains a list of products (Product[]) that you can render to the user.

Once you’ve built one of the controllers listed in the preceding section and have access to state.products, you can pass the products to a component that will render them.

Note

For an example of how to initialize the Search controller and access products to pass to the ProductList component in the following code snippet, see Displaying the search page with the Search controller.

The following example ProductList component receives a list of products and renders them:

import {
  InteractiveProduct as HeadlessInteractiveProduct,
  InteractiveProductProps,
  Product as HeadlessProduct,
  Cart,
} from '@coveo/headless/commerce';
import InteractiveProduct from '../interactive-product/interactive-product';

interface IProductListProps {
  products: HeadlessProduct[]; 1
  controllerBuilder: ( 2
    props: InteractiveProductProps
  ) => HeadlessInteractiveProduct;
  navigate: (pathName: string) => void; 3
}

export default function ProductList(props: IProductListProps) {
  const {products, controllerBuilder, navigate} = props;

  if (products.length === 0) {
    return null;
  }

  return (
    <ul>
      {products.map((product, index) => ( 4
        <li key={index}>
          <InteractiveProduct 5
            product={product}
            controller={controllerBuilder({options: {product}})}
            navigate={navigate}
          ></InteractiveProduct>
        </li>
      ))}
    </ul>
  );
}
1 ProductList takes a list of products as input, accessed from the product discovery solution controller. This component is agnostic to the specific controller used.
2 A function that builds the controller for the InteractiveProduct component. This function returns an instance of HeadlessInteractiveProduct, which logs the correct analytics event when a product is clicked. It can be accessed through the controller.interactiveProduct method on the product discovery solution controller.
3 Function to navigate to a specific path in the application. This function will be used to navigate to the PDP when a product is clicked.

The implementation details of this function are specific to the application and aren’t covered in this example.

4 Loops through the list of products and renders a component for each product.
5 InteractiveProduct is a custom component that renders a product and logs the correct analytics event when the product is clicked.

For more details on how to implement the InteractiveProduct component, see the next section.

Using the InteractiveProduct sub-controller

Every product displayed in a product list—whether on a search page, a product listing page, a recommendations carousel, or instant products displayed below the search box—must use the InteractiveProduct sub-controller to ensure that the correct analytics event is logged when the product is clicked.

The following code snippet demonstrates how to create an InteractiveProduct component that displays a product and uses the InteractiveProduct sub-controller to log the click event when a product is clicked:

import {
  InteractiveProduct as HeadlessInteractiveProduct,
  Product,
} from '@coveo/headless/commerce';

interface IInteractiveProductProps {
  product: Product;
  controller: HeadlessInteractiveProduct;
  navigate: (pathName: string) => void;
}

export default function InteractiveProduct(props: IInteractiveProductProps) {
  const {product, controller, navigate} = props;

  const clickProduct = () => {
    controller.select(); 1
    const productId = product.ec_product_id ?? product.permanentid;
    navigate(`/product/${productId}`); 2
  };

  return ( 3
    <div>
      <button onClick={clickProduct}>
        {product.ec_name}
      </button>
      <div>
        <img
          src={product.ec_images[0]}
          alt={product.permanentid}
          height={100}
        ></img>
      </div>
       {product.ec_promo_price}
      <div>
        <p>{product.ec_description}</p>
      </div>
    </div>
  );
}
1 Logs the click event using the InteractiveProduct controller when the product is clicked. This controller is passed to the component from the ProductList component.
2 Navigates to the product detail page when the product is clicked. This function is passed to the component from the ProductList component, and the implementation details for this function vary depending on the application.
3 Renders the product information and a button that, when clicked, logs the click event and navigates to the product detail page.

In the preceding code sample, the logic to interact with the cart and log events based on user interaction has been omitted for brevity.

For more details on implementing this functionality with Headless, see Managing the cart and the sample application in the Headless repository.

Displaying badges on linked placements

If you’ve configured badges in the Coveo Merchandising Hub (CMH) Badge manager for linked placements, such as search results, product listing pages, or recommendation slots, badges are returned as part of the product data on every product discovery controller (Search, ProductListing, and Recommendations).

In this case, only a small amount of additional rendering code is needed. You display badges in your InteractiveProduct component the same way you display any other product attribute, such as ec_name or ec_price, by reading them directly from the product object.

Note

This subsection covers linked placements only. To display badges on a product detail page (PDP) or another standalone placement, see Displaying badges on a product detail page.

Badges are exposed on the product via product.badgePlacements, an array of placements each containing the badges that apply to the product for that placement. Use this property when you want to render badges grouped by their CMH placement (for example, when one component renders multiple placement IDs in different positions).

Badge text returned in product.badgePlacements already has dynamic social proof figures resolved (for example, {{purchases}} is replaced with the actual purchase count). You can render badge.text directly without any additional processing.

The following example extends the InteractiveProduct component from the preceding section to render badges from product.badgePlacements:

export default function InteractiveProduct(props: IInteractiveProductProps) {
  const {product, controller, navigate} = props;

  const clickProduct = () => {
    controller.select();
    const productId = product.ec_product_id ?? product.permanentid;
    navigate(`/product/${productId}`);
  };

  return (
    <div>
      <button onClick={clickProduct}>
        {product.ec_name}
      </button>
      <div>
        <img
          src={product.ec_images[0]}
          alt={product.permanentid}
          height={100}
        ></img>
      </div>
      {product.ec_promo_price}
      <div>
        <p>{product.ec_description}</p>
      </div>
      {product.badgePlacements?.map((placement) => ( 1
        <div key={placement.placementId}>
          {placement.badges.map((badge) => ( 2
            <span
              key={badge.id}
              style={{
                backgroundColor: badge.backgroundColor,
                color: badge.textColor,
              }}
            >
              {badge.iconUrl && (
                <img src={badge.iconUrl} alt="Badge icon" width={20} />
              )}
              {badge.text} 3
            </span>
          ))}
        </div>
      ))}
    </div>
  );
}
1 Iterates over the placements that apply to this product. If the product doesn’t qualify for any badges, product.badgePlacements is an empty array and nothing is rendered.
2 Iterates over the badges within each placement. The CMH-configured priority order and Max visible badges limit are already applied, so you can render the array as-is.
3 Renders the badge text. Dynamic metric placeholders (for example, {{purchases}}, {{views}}, {{addToCarts}}) are already resolved by the time you receive the data.

For information on creating and managing badges in CMH, see Badges and Placements.

Displaying a product on a product detail page

When displaying a product on a product detail page (PDP), use the ProductView controller to ensure that the correct product view event is logged when a user views a product.

When the user navigates to the PDP, display the product details and use the ProductView controller to log the product view event.

import {
  CommerceEngine,
  Context,
  buildProductView,
} from '@coveo/headless/commerce';
import {useEffect, useRef} from 'react';
import {loadProduct} from '../utils/pdp-utils';

interface IProductDescriptionPageProps {
  engine: CommerceEngine;
  contextController: Context;
  url: string;
  navigate: (pathName: string) => void;
}

export default function ProductDescriptionPage(props: IProductDescriptionPageProps) {
  const {engine, contextController, url, navigate} = props;

  const productViewEventEmitted = useRef(false); 1
  const product = loadProduct(); 2

  useEffect(() => { 3
    contextController.setView({url});
  }, [contextController, url]);

  useEffect(() => { 4
    if (productViewEventEmitted.current || !product) {
      return;
    }

    buildProductView(engine).view(product);
    productViewEventEmitted.current = true;
  }, [engine, product]);

  if (!product) {
    return null;
  }

  return ( 5
    <div >
      <h2>{product.name}</h2>
      <p>Price: {product.price}</p>
    </div>
  );
}
1 Use the useRef hook to track whether the product view event has been logged for the product displayed on the PDP. This hook ensures that the product view event is only logged once per product per page load.
2 The loadProduct function is a placeholder for logic that fetches the product data from a backend service. Implementation details will vary based on your application.
3 Sets the context to keep the state of the URL in sync with Headless.

For more information on why this is important and how to do it, see Navigating between pages.

4 Logs the product view event when the component mounts, if the event hasn’t already been logged and if the product data is available.

Use the buildProductView function to create an instance of the ProductView controller and call the view method by passing in the product data to log the product view event.

5 Renders the product information on the PDP.

PDP pages typically also include functionality that allows products to be added to the cart. This functionality has been omitted for brevity.

For more details on implementing this functionality with Headless, see Managing the cart and the sample application in the Headless repository.

Displaying badges on a product detail page

If you’ve configured badges in the Coveo Merchandising Hub (CMH) Badge manager, you can display them on your product detail page (PDP) using the ProductEnrichment controller.

Badges created in CMH can be displayed in two ways:

  • Linked placements (search results, product listing pages): Badge data is automatically included in product discovery controller responses (such as Search, ProductListing, or Recommendations). Implementing a placement consists of displaying badges from product.badgePlacements in your product list components.

  • Standalone placements (product detail pages (PDPs)): Badge data must be fetched separately using the ProductEnrichment controller. This section explains how to implement standalone badges for PDPs.

Note

For information on creating and managing badges in CMH, see Badges and Placements.

Prerequisites

  • Badges and standalone placements configured in the CMH Badge manager.

  • Headless library version 3.37.0 or later.

  • Placement IDs from CMH for the standalone placements you want to display.

Fetching badges for a product

Use the ProductEnrichment controller to fetch badges for a specific PDP product by passing the product identifier and standalone placement IDs:

import {
  buildProductEnrichment,
  BadgePlacement,
  type CommerceEngine,
} from '@coveo/headless/commerce';
import {useState, useEffect} from 'react';

interface IProductBadgesProps {
  engine: CommerceEngine;
  productId: string;
  placementIds: string[]; 1
}

export default function ProductBadges(props: IProductBadgesProps) {
  const {engine, productId, placementIds} = props;
  const [productBadgePlacements, setProductBadgePlacements] = useState<BadgePlacement[] | undefined>(undefined);

  // Initialize controller with product ID and placement ID(s) from CMH
  const productEnrichmentController = buildProductEnrichment(engine, {
    options: {
      productId: productId ?? '', 2
      placementIds: placementIds, 3
    },
  });

  // Fetch badges when component mounts
  useEffect(() => { 4
    if (!productEnrichmentController.state.isLoading && !productBadgePlacements && !productEnrichmentController.state.error) {
      productEnrichmentController.getBadges();
    }
  }, [productEnrichmentController, productBadgePlacements]);

  // Subscribe to state changes and update badges
  useEffect(() => { 5
    const unsubscribe = productEnrichmentController.subscribe(() => {
      const productWithBadges = productEnrichmentController.state.products.find(
        (p) => p.productId === productId
      );
      setProductBadgePlacements(productWithBadges ? productWithBadges.badgePlacements : []);
    });

    return unsubscribe;
  }, [productEnrichmentController, productId]);

  if (!productBadgePlacements || productBadgePlacements.length === 0) { 6
    return null;
  }

  return ( 7
    <div className="product-badges">
      {productBadgePlacements.map((placement, placementIndex) => (
        <div key={placementIndex}>
          {placement.badges.map((badge, badgeIndex) => (
            <div
              key={badgeIndex}
              style={{
                backgroundColor: badge.backgroundColor,
                color: badge.textColor,
              }}
            >
              {badge.iconUrl && (
                <img src={badge.iconUrl} alt="Badge icon" width={20} />
              )}
              <span>{badge.text}</span>
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}
1 Placement IDs must match the standalone placement IDs configured in CMH. You can retrieve these from the CMH Placements tab.
2 The product ID for which to fetch badges. This should match the product ID used in your catalog data.
3 Array of placement IDs to fetch badges for. You can specify multiple placements if needed (for example, multiple PDP badge locations).
4 Fetches badges when the component mounts, if they haven’t already been fetched and if there’s no error.
5 Subscribes to controller state changes to update the component when badges are loaded.
6 If no badges are configured or the product doesn’t qualify for any badges, render nothing.
7 Renders badges using the styling configured in CMH (background color, text color, optional image).
Note

The ProductEnrichment controller automatically handles dynamic social proof figures in badge text. If a badge in CMH includes placeholders like {{purchases}} or {{views}}, the controller replaces them with actual values for each product.

Alternative: Using the Commerce API directly

If you’re not using the Headless library, you can fetch badges directly from the Commerce API.

For more information, see:

Headless reference

For more information on the ProductEnrichment controller and its capabilities, see: