Build product listing pages (Shopify Hydrogen)

This is for:

Developer

Building product listing pages (PLPs) with Coveo Headless involves three main elements:

  1. Keeping the URL up to date with the Headless state as you navigate between pages by using a hook on the ParameterManager controller.

  2. Fetching and displaying listing results by using a hook on the ProductList controller.

  3. Wrapping your components with a listing provider so that you can manage the Headless listing state and hydration.

Prerequisites

Keeping URL up to date

To ensure the correct listing page configuration is fetched when navigating between pages, you must keep the URL state updated in Headless.

The loader uses fetchStaticState to sync the URL state with the interface. It’s crucial to pass the correct URL as it’s assigned to context.view and associated with the listing page configuration to fetch products.

// ...
const staticState = await fetchStaticState({
  url: `https://shop.barca.group/plp/${params['*']}`, 1
  context,
  parameters,
  k: 'listingEngineDefinition',
  request,
});
// ...
1 Must specify the URL that corresponds to the PLP configuration you want to target.

Managing URL parameters

The Headless React package provides a ParameterManager controller that lets you manage URL parameters in your commerce application.

For more information, see managing parameters.

Define and retrieve a product list controller hook

While defining your commerce engine, use the defineProductList function to define and retrieve the target product list hook.

// app/lib/commerce-engine-config.ts

import { defineCommerceEngine } from '@coveo/headless-react/ssr-commerce';
import {
    defineProductList,
    // ...
} from '@coveo/headless-react/ssr-commerce';

// ...

export default {
    // ...
    controllers: {
        productList: defineProductList(), 1
        // ...
    },
    //...
};

// ...

export const {
    useProductList, 2
    // ...
} = engineDefinition.controllers;
1 Define the productList controller using the defineProductList function.
2 You can use the same useProductList hook for both search and listing pages.

Display listing page results with the listing provider

Render your listing page results using your hook on the ProductList controller, and wrap your components with your listing provider.

import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {useLoaderData, useParams, type MetaFunction} from '@remix-run/react';
import {
  ClientSideNavigatorContextProvider,
  ServerSideNavigatorContextProvider,
} from '~/lib/navigator.provider';
import {
  engineDefinition,
  fetchStaticState,
  type ListingStaticState,
} from '~/lib/coveo.engine';
import {ListingProvider} from '~/components/Search/Context';
import {buildParameterSerializer} from '@coveo/headless-react/ssr-commerce';
import {useEffect, useState} from 'react';
import ParameterManager from '~/components/ParameterManager';

// ...

export async function loader({context, params, request}: LoaderFunctionArgs) {
  const {listingEngineDefinition} = engineDefinition;
  const url = new URL(request.url);
  const {deserialize} = buildParameterSerializer();
  const parameters = deserialize(url.searchParams); 1
  listingEngineDefinition.setNavigatorContextProvider( 2
    () => new ServerSideNavigatorContextProvider(request),
  );

  const staticState = await fetchStaticState({ 3
    url: `https://shop.barca.group/plp/${params['*']}`,
    context,
    parameters,
    k: 'listingEngineDefinition',
    request,
  });

  return {staticState, url};
}

export default function PLP() {
  const {staticState, url} = useLoaderData<typeof loader>();
  const [currentUrl, setCurrentUrl] = useState(url);

  useEffect(() => {
    setCurrentUrl(window.location.href);
  }, []);

  // ...

  return (
    <ListingProvider 4
      navigatorContext={new ClientSideNavigatorContextProvider()}
      staticState={staticState as ListingStaticState}
    >
      <ParameterManager url={currentUrl} /> 5
      <section>
        <div>
          <div>
            <Sorts /> 6
          </div>

          <div ref={facetsContainer}>
            <Facets numFacetsInLine={numFacetsInline} /> 7
          </div>
        </div>

        <Breadcrumbs /> 8
      </section>

      <section>
        <ProductList /> 9
        <PaginationFooter /> 10
      </section>
    </ListingProvider>
  );
}
1 Deserialize the URL parameters using the deserialize function returned by the buildParameterSerializer function.
2 Create a navigation context provider.
3 Fetch the static state for the listing page with the deserialized parameters as the initial value.
4 Wrap your components in a listing provider.
5 Use your ParameterManager component.
6 Display the sorting options generated by the Sort controller.
7 Display the facets generated by the FacetGenerator controller.
8 Displays a summary of the currently active facet values via the BreadcrumbManager.
9 Render the list of products returned by the query. The ProductList component relies on the useProductList hook to fetch and display the products.
10 Display the pagination generated by the Pagination controller.

For the complete example, see the Barca Sports repository.