Displaying products (SSR)
Displaying products (SSR)
|
|
The Headless Commerce SSR utilities are in open beta. Contact your Coveo representative for support in adopting this. |
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 storefront, 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 through controller hooks
Controller hooks let you interact with products and display them in your storefront.
Define target controller hooks
Define and use controller hooks specific to the product discovery solution you’re implementing.
| Context | Function to define the controller hook |
|---|---|
Search page |
|
Listing page |
|
Recommendation interface |
Commerce engine configuration example:
// lib/commerce-engine-config.ts
import {
defineProductList,
defineRecommendations,
// ...
} from '@coveo/headless-react/ssr-commerce';
export default {
// ...
controllers: {
productList: defineProductList(),
popularViewed: defineRecommendations({
options: {
slotId: 'd73afbd2-8521-4ee6-a9b8-31f064721e73',
},
}),
popularBought: defineRecommendations({
options: {
slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174',
},
}),
viewedTogether: defineRecommendations({
options: {
slotId: 'ff5d8804-d398-4dd5-b68c-6a729c66454b',
},
}),
// ...
},
// ...
};
Commerce engine definition example:
// lib/commerce-engine.ts
import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce';
import engineConfig from './commerce-engine-config';
export const engineDefinition = defineCommerceEngine(engineConfig);
// ...
export const {
useProductList,
usePopularBought,
usePopularViewed,
useViewedTogether,
// ...
} = engineDefinition.controllers;
For complete code samples, see commerce-engine-config.ts and commerce-engine.ts.
Access products with target hooks
Regardless of the controller hook you use, you can display products by accessing the state.products object on the controller hook.
This object contains a list of products (Product[]) that you can render to the user.
Once you’ve retrieved one of the controller hooks listed above and have access to state.products, you can pass the products to a component that will render them.
The following example ProductList component receives a list of products and renders them using a ProductButtonWithImage component, defined in the following section.
It’s meant to work for product listing search pages.
// components/product-list.tsx
'use client';
import {
useProductList
// ...
} from '@/lib/commerce-engine';
import ProductButtonWithImage from './product-button-with-image';
// ...
export default function ProductList() {
const {state, methods} = useProductList();
// ...
return (
<ul aria-label="Product List">
{state.products.map((product) => (
<li key={product.ec_product_id}>
<ProductButtonWithImage methods={methods} product={product} />
<button
onClick={
() =>
}
>
Add to cart
</button>
</li>
))}
</ul>
);
}
| The logic to interact with the cart and log events based on user interaction has been omitted for brevity. For a complete sample, see the Headless project repository. |
To display recommendations, you would use the usePopularBought, usePopularViewed, or useViewedTogether controller hooks instead of useProductList.
// components/recommendations/popular-bought.tsx
'use client';
import {usePopularBought} from '@/lib/commerce-engine';
import ProductButtonWithImage from './product-button-with-image';
export default function PopularBought() {
const {state, methods} = usePopularBought();
return (
<>
<ul>
<h3>{state.headline}</h3>
{state.products.map((product) => (
<li key={product.ec_product_id}>
<ProductButtonWithImage methods={methods} product={product} />
</li>
))}
</ul>
</>
);
}
|
|
Note
Also, to display recommendations, make sure to enable the target recommendation hook when fetching the static state. See Display recommendations with your recommendation provider. |
Display the target product
Whether it’s on a search page, a product listing page, a recommendation carousel, or as an instant product displayed below the search box, every product displayed in a product list must use the interactiveProduct.select function to ensure that the correct analytics event is logged when the product is clicked.
// components/product-button-with-image.tsx
import {
Product,
ProductList,
Recommendations,
} from '@coveo/headless-react/ssr-commerce';
import Image from 'next/image';
import {useRouter} from 'next/navigation';
export interface ProductButtonWithImageProps {
methods:
| Omit<Recommendations, 'state' | 'subscribe'>
| Omit<ProductList, 'state' | 'subscribe'>
| undefined;
product: Product;
}
export default function ProductButtonWithImage({
methods,
product,
}: ProductButtonWithImageProps) {
const router = useRouter();
const onProductClick = (product: Product) => {
methods?.interactiveProduct({options: {product}}).select();
router.push(
`/products/${product.ec_product_id}?name=${product.ec_name}&price=${product.ec_price}`
);
};
return (
<button disabled={!methods} onClick={() => onProductClick(product)}>
{product.ec_name}
<Image
src={product.ec_images[0]}
alt={product.ec_name!}
width={50}
height={50}
/>
</button>
);
}
The methods prop is a union of the methods available through the Recommendations and ProductList controller hooks.
This lets the component work for both use cases.
The undefined type is for the case of static rendering, before hydration, when the hook isn’t yet available. |
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).
Only a small amount of additional rendering code is needed.
You display badges the same way you display any other product attribute, such as ec_name or ec_price, by reading them directly from the product object passed to your ProductButtonWithImage component.
|
|
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 ProductButtonWithImage component from the preceding section to render badges from product.badgePlacements:
import {
Product,
ProductList,
Recommendations,
} from '@coveo/headless-react/ssr-commerce';
import Image from 'next/image';
import {useRouter} from 'next/navigation';
export interface ProductButtonWithImageProps {
methods:
| Omit<Recommendations, 'state' | 'subscribe'>
| Omit<ProductList, 'state' | 'subscribe'>
| undefined;
product: Product;
}
export default function ProductButtonWithImage({
methods,
product,
}: ProductButtonWithImageProps) {
const router = useRouter();
const onProductClick = (product: Product) => {
methods?.interactiveProduct({options: {product}}).select();
router.push(
`/products/${product.ec_product_id}?name=${product.ec_name}&price=${product.ec_price}`
);
};
return (
<button disabled={!methods} onClick={() => onProductClick(product)}>
{product.ec_name}
<Image
src={product.ec_images[0]}
alt={product.ec_name!}
width={50}
height={50}
/>
{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>
))}
</button>
);
}
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), define and use a hook on the ProductView controller to ensure that the correct product view event is logged when a user views a product.
Create a product viewer component that you’ll then include in your product detail pages.
'use client';
import {useProductView} from '@/lib/commerce-engine';
import {useEffect} from 'react';
interface Product {
productId: string;
name: string;
price: number;
}
export default function ProductViewer({productId, name, price}: Product) {
const {methods} = useProductView();
const productViewEventEmitted = false;
useEffect(() => {
if (methods && !productViewEventEmitted) {
methods?.view({productId, name, price});
productViewEventEmitted = true;
}
}, []);
return null;
}
| A flag so the product view event is logged only once. |
Then, include the ProductViewer component in your product detail page component.
// ...
import ProductViewer from '@/components/product-viewer';
export default async function ProductDescriptionPage({
params,
searchParams,
}: {
params: {productId: string};
searchParams: Promise<{[key: string]: string | string[] | undefined}>;
}) {
// ...
const resolvedSearchParams = await searchParams;
const price = Number(resolvedSearchParams.price) ?? NaN;
const name = Array.isArray(resolvedSearchParams.name)
? params.productId
: (resolvedSearchParams.name ?? params.productId);
return (
<StandaloneProvider
staticState={staticState}
navigatorContext={navigatorContext.marshal}
>
<ProductViewer productId={params.productId} name={name} price={price} />
<!--- ... --->
</StandaloneProvider>
);
}
export const dynamic = 'force-dynamic';
Wrap your product viewer component in a StandaloneProvider to handle static rendering and hydration.
See Headless commerce usage (SSR): Create providers. |
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.
|
|
Note
This section covers standalone placements (product detail pages (PDPs)) that require separate badge fetching. For linked placements (search results, product listing pages), badges are automatically included in product discovery responses. See About placements for more information on placement types. |
Prerequisites
-
Badges and standalone placements configured in CMH Badge manager.
-
Headless React library version 3.40.0 or later.
-
Placement IDs from CMH for the standalone placements you want to display.
Configure the controller
Define the ProductEnrichment controller in your commerce engine configuration.
// lib/commerce-engine-config.ts
import {
defineProductEnrichment,
type CommerceEngineDefinitionOptions,
} from '@coveo/headless-react/ssr-commerce';
export default {
configuration: {
// Your engine configuration
},
controllers: {
productEnrichment: defineProductEnrichment(),
// ... other controllers
},
} satisfies CommerceEngineDefinitionOptions;
Define the productEnrichment controller without options.
The productId and placementIds are set per-request when fetching static state. |
Commerce engine definition example:
import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce';
import engineConfig from './commerce-engine-config';
export const standaloneEngineDefinition = defineCommerceEngine(engineConfig);
// ...
export const {
useProductEnrichment,
// ...
} = standaloneEngineDefinition.controllers;
Fetch badges on the server
Fetch badge data on the server side by configuring the productEnrichment options when fetching static state.
import {StandaloneProvider} from '@coveo/headless-react/ssr-commerce';
import {standaloneEngineDefinition} from '@/lib/commerce-engine';
import ProductBadges from '@/components/product-badges';
export default async function ProductPage({
params,
}: {
params: {productId: string};
}) {
const {productId} = params;
// Fetch static state with productEnrichment options
const staticState = await standaloneEngineDefinition.fetchStaticState({
controllers: {
productEnrichment: {
options: {
productId: productId,
placementIds: ['xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'],
},
},
context: {
view: {
url: `https://your-site.com/products/${productId}`,
},
// ... other context
},
},
});
return (
<StandaloneProvider staticState={staticState}>
<h1>Product Details</h1>
<ProductBadges />
</StandaloneProvider>
);
}
Fetch static state with the productEnrichment controller configured for this specific request. |
|
| Provide the product ID for the product being displayed. While optional, this should always be provided on product detail pages (PDPs) to ensure badges are fetched for the correct product. | |
Provide placement IDs from CMH. This parameter is required - the controller will throw an error when calling getBadges() if no placement IDs are provided. You can retrieve placement IDs from the CMH Placements tab. |
|
Pass the static state to the StandaloneProvider to make badge data available to client components. |
|
|
Required parameter
The |
Display badges in a client component
Use the useProductEnrichment hook in a client component to request badges via methods.getBadges() and render SSR-cached results or fetch fresh data when needed.
Create a client component to render the badges using the useProductEnrichment hook.
'use client';
import {useEffect} from 'react';
import {useProductEnrichment} from '@/lib/commerce-engine';
export default function ProductBadges() {
const {state, methods} = useProductEnrichment();
// Fetch badges on mount (uses cached SSR data if available)
useEffect(() => {
if (methods === undefined) return;
methods.getBadges();
}, [methods]);
// Flatten all badges from all products and placements
const allBadges = state.products.flatMap((product) =>
product.badgePlacements.flatMap((placement) =>
placement.badges.map((badge) => ({
key: `${product.productId}-${placement.placementId}-${badge.text}`,
badge,
}))
)
);
if (state.isLoading) {
return <div>Loading badges...</div>;
}
if (state.error) {
console.error('Error loading badges:', state.error);
return null;
}
return (
<div style={{display: 'flex', flexWrap: 'wrap', gap: '8px'}}>
{allBadges.map(({key, badge}) => (
<div
key={key}
style={{
backgroundColor: badge.backgroundColor,
color: badge.textColor,
padding: '4px 8px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}
>
{badge.iconUrl && <img src={badge.iconUrl} alt="Badge image" width={20} />}
<span>{badge.text}</span>
</div>
))}
</div>
);
}
| Fetch badges when the component mounts. This uses the cached SSR data if available or makes a fresh request if needed. | |
Flatten the badge data structure to easily iterate over all badges.
The ProductEnrichment controller returns products, each with badge placements, each with badges. |
|
| Show a loading state while badges are being fetched. | |
| Handle errors gracefully. Log the error for debugging but don’t display an error message to the user. | |
Render badges using the styling configured in CMH (background color, text color, optional icon). Dynamic figures in badge text (such as {{purchases}} or {{views}}) are automatically replaced with actual values before rendering. |
|
|
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: