Displaying products (CSR)
Displaying products (CSR)
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.
-
To display products returned from Search, use the
Searchcontroller. -
To display products on a listing page, use the
ProductListingcontroller. -
To display products in a recommendation interface, use the
Recommendationscontroller.
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 |
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[];
controllerBuilder: (
props: InteractiveProductProps
) => HeadlessInteractiveProduct;
navigate: (pathName: string) => void;
}
export default function ProductList(props: IProductListProps) {
const {products, controllerBuilder, navigate} = props;
if (products.length === 0) {
return null;
}
return (
<ul>
{products.map((product, index) => (
<li key={index}>
<InteractiveProduct
product={product}
controller={controllerBuilder({options: {product}})}
navigate={navigate}
></InteractiveProduct>
</li>
))}
</ul>
);
}
ProductList takes a list of products as input, accessed from the product discovery solution controller.
This component is agnostic to the specific controller used. |
|
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. |
|
| 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. |
|
| Loops through the list of products and renders a component for each product. | |
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 |
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();
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>
</div>
);
}
Logs the click event using the InteractiveProduct controller when the product is clicked.
This controller is passed to the component from the ProductList component. |
|
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. |
|
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) => (
<div key={placement.placementId}>
{placement.badges.map((badge) => (
<span
key={badge.id}
style={{
backgroundColor: badge.backgroundColor,
color: badge.textColor,
}}
>
{badge.iconUrl && (
<img src={badge.iconUrl} alt="Badge icon" width={20} />
)}
{badge.text}
</span>
))}
</div>
))}
</div>
);
}
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. |
|
| 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. | |
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);
const product = loadProduct();
useEffect(() => {
contextController.setView({url});
}, [contextController, url]);
useEffect(() => {
if (productViewEventEmitted.current || !product) {
return;
}
buildProductView(engine).view(product);
productViewEventEmitted.current = true;
}, [engine, product]);
if (!product) {
return null;
}
return (
<div >
<h2>{product.name}</h2>
<p>Price: {product.price}</p>
</div>
);
}
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. |
|
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. |
|
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. |
|
| 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 |
|
| 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, orRecommendations). Implementing a placement consists of displaying badges fromproduct.badgePlacementsin your product list components. -
Standalone placements (product detail pages (PDPs)): Badge data must be fetched separately using the
ProductEnrichmentcontroller. 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[];
}
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 ?? '',
placementIds: placementIds,
},
});
// Fetch badges when component mounts
useEffect(() => {
if (!productEnrichmentController.state.isLoading && !productBadgePlacements && !productEnrichmentController.state.error) {
productEnrichmentController.getBadges();
}
}, [productEnrichmentController, productBadgePlacements]);
// Subscribe to state changes and update badges
useEffect(() => {
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) {
return null;
}
return (
<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>
);
}
| Placement IDs must match the standalone placement IDs configured in CMH. You can retrieve these from the CMH Placements tab. | |
| The product ID for which to fetch badges. This should match the product ID used in your catalog data. | |
| Array of placement IDs to fetch badges for. You can specify multiple placements if needed (for example, multiple PDP badge locations). | |
| Fetches badges when the component mounts, if they haven’t already been fetched and if there’s no error. | |
| Subscribes to controller state changes to update the component when badges are loaded. | |
| If no badges are configured or the product doesn’t qualify for any badges, render nothing. | |
| Renders badges using the styling configured in CMH (background color, text color, optional image). |
|
|
Note
The |
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: