Core concepts (Shopify Hydrogen)

This is for:

Developer

What is Coveo Headless?

The Coveo Headless library is a Redux-based toolset for developing UI components. It provides utilities for interacting with the Coveo Platform and building user interfaces for various product discovery solutions.

The @coveo/headless-react package provides utilities for React that are compatible with a Hydrogen storefront.

You should use the Headless library instead of working directly with the REST APIs. Headless abstracts the complexity of Coveo’s APIs and provides a developer-friendly interface. Under the hood, Headless uses the Commerce API to query products from Coveo and the Event Protocol to send Coveo Analytics events.

Server-side rendering

Server-side rendering (SSR) is a technique that allows web applications to render on the server before sending them to the client. This approach is particularly useful when developing with the Coveo Headless framework because it enables faster initial loading times and better SEO. By rendering the HTML on the server, the client can receive a fully formed page which is ready to be displayed as soon as it’s received.

At a high level, the process looks like the following:

SSR diagram | Coveo for Commerce

Headless engine and controllers

The Headless commerce engine manages the state of the product discovery solution interface and communicates with the Coveo Platform. The Headless commerce engine is defined as a singleton, meaning a single instance is shared between the server and client.

A Headless controller is an abstraction that simplifies the implementation of a specific Coveo-powered UI feature or component. Controllers provide programming interfaces for interacting with the Headless engine’s state.

Example

To implement the cart component, use the Cart controller, available in the @coveo/headless-react/ssr-commerce package.

This controller exposes public methods such as purchase and empty.

Write code to call the appropriate controller methods when a user completes a purchase or empties the cart. These methods update the cart state in the commerce engine and trigger the necessary API calls to the Coveo Platform.

When defining your engine, specify the controllers to use in your commerce site. On the client side, use controller hooks to interact with the engine in your components.

Initializing the engine

Initialize the Headless engine by first defining its configuration and controllers.

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

import {
  type CommerceEngineDefinitionOptions,
  defineBreadcrumbManager,
  defineCart,
  defineContext,
  defineFacetGenerator,
  defineInstantProducts,
  definePagination,
  defineParameterManager,
  defineProductList,
  defineRecommendations,
  defineSearchBox,
  defineSort,
  defineStandaloneSearchBox,
  defineSummary,
  getSampleCommerceEngineConfiguration, 1
} from '@coveo/headless-react/ssr-commerce'; 2
import {
  CART_SLOT_ID,
  HOMEPAGE_SLOT_ID,
  PDP_LOWER_CAROUSEL_SLOT_ID,
  PDP_UPPER_CAROUSEL_SLOT_ID,
} from './recommendation-slots';
export default {
  configuration: {
    ...getSampleCommerceEngineConfiguration(), 3
  },
  controllers: { 4
    context: defineContext(),
    cart: defineCart(),
    summary: defineSummary(),
    searchBox: defineSearchBox(), 5
    standaloneSearchBox: defineStandaloneSearchBox({
      options: {redirectionUrl: '/search', id: 'standalone-search-box'},
    }),
    instantProducts: defineInstantProducts(), 6
    productList: defineProductList(), 7
    parameterManager: defineParameterManager(), 8
    facetGenerator: defineFacetGenerator(),
    sort: defineSort(),
    pagination: definePagination(),
    breadcrumbManager: defineBreadcrumbManager(),
    homepageRecommendations: defineRecommendations({
      options: {slotId: HOMEPAGE_SLOT_ID},
    }), 9
    cartRecommendations: defineRecommendations({
      options: {slotId: CART_SLOT_ID},
    }),
    pdpRecommendationsLowerCarousel: defineRecommendations({
      options: {slotId: PDP_LOWER_CAROUSEL_SLOT_ID},
    }),
    pdpRecommendationsUpperCarousel: defineRecommendations({
      options: {slotId: PDP_UPPER_CAROUSEL_SLOT_ID},
    }),
  },
} satisfies CommerceEngineDefinitionOptions;
1 getSampleCommerceEngineConfiguration returns a pre-configured set of demo credentials. Replace this with your actual Coveo commerce configuration before deploying.
2 Import the necessary controller definers for the commerce engine.
3 Spreads the sample configuration. In production, replace this call with your own organizationId, accessToken, analytics, and context values.
4 Specify the controllers to define as part of the commerce engine definition.
5 The searchBox controller powers a search input on the search results page. The standaloneSearchBox is for pages that redirect to search.
6 The instantProducts controller displays relevant products in real time as users type in the search box.
7 The productList controller provides search results as a list of products.
8 The parameterManager controller synchronizes URL parameters (such as the q query parameter) with the engine state.
9 For each recommendation controller, specify the slot ID of the recommendation slot configuration from your Coveo organization. The slot IDs are defined in recommendation-slots.ts.

Next, use your engine configuration to define the commerce engine, as in the following example:

// app/lib/commerce-engine.ts

import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce';
import engineConfig from './commerce-engine-config';

export const engineDefinition = defineCommerceEngine(engineConfig); 1

export const {
  listingEngineDefinition,
  searchEngineDefinition,
  recommendationEngineDefinition,
  standaloneEngineDefinition,
  useEngine,
} = engineDefinition; 2
export const {
  useContext,
  useCart,
  useSummary,
  useSearchBox,
  useProductList,
  useParameterManager,
  useStandaloneSearchBox,
  useInstantProducts,
  useFacetGenerator,
  useSort,
  usePagination,
  useBreadcrumbManager,
  useHomepageRecommendations,
  useCartRecommendations,
  usePdpRecommendationsLowerCarousel,
  usePdpRecommendationsUpperCarousel,
} = engineDefinition.controllers; 3
export {fetchStaticState} from './fetch-static-state-simple'; 4
1 Define the commerce engine using the target commerce engine configuration.
2 Engines for different use cases in your commerce site. These engine definitions will be imported in your components to access the engine state.
3 Retrieve the controller hooks created based on the controllers definitions you provided in the commerce engine configuration.
4 Re-export fetchStaticState so that route files can import everything from a single module.

What is static state and how is it used by the client?

Static state refers to a precomputed snapshot of the Coveo Headless engine’s state, fetched on the server before sending the page to the client. This ensures that when the client receives the page, it already contains the necessary search parameters, filters, cart state, and navigation data without requiring an additional request.

Static state is essential for SSR because it enables fast page loads while ensuring that search-related UI components are correctly populated.

Additionally, hydration is the process of reusing the static state on the client side. This allows the search engine to seamlessly continue from where the server left off without needing to fetch data again.

Using providers

Providers establish the context and manage state for a Coveo Headless engine within a Hydrogen application. They ensure that the engine has the necessary navigation data and static state for server-side rendering (SSR) while enabling client-side hydration for dynamic updates.

To enable SSR with Coveo Headless and Hydrogen, you must create two types of providers:

Navigation context providers wrap relevant navigation context information and make it available to the Headless engine.

They collect key information about the user’s environment, such as:

  • Referrer: The previous page’s URL (if available).

  • User agent: Information about the user’s browser or device.

  • Location: The current URL being accessed.

  • client ID: A unique identifier for the user’s session or visit.

Depending on whether navigation context is fetched on the client or server, you must create two different providers.

Server-side and client navigation context providers

The following example shows how to create a server-side and client-side navigation context provider:

// app/lib/navigator-provider.ts

import type {NavigatorContext} from '@coveo/headless-react/ssr-commerce';
import {getCookie, getCookieFromRequest} from './session';

export class ServerSideNavigatorContextProvider implements NavigatorContext { 1
  private request: Request;
  private generatedId?: string;
  constructor(request: Request) {
    this.request = request;
  }
  get referrer() {
    return this.request.referrer || '';
  }

  get userAgent() {
    return this.request.headers.get('user-agent') || '';
  }

  get location() {
    return this.request.url || '';
  }

  get clientId() { 2
    const idFromRequest = getCookieFromRequest(this.request, 'coveo_visitorId');
    if (idFromRequest) {
      return idFromRequest;
    }
    if (this.generatedId) {
      return this.generatedId;
    }
    const generated = crypto.randomUUID();
    this.generatedId = generated;
    return generated;
  }

  get marshal(): NavigatorContext {
    return {
      clientId: this.clientId,
      location: this.location,
      referrer: this.referrer,
      userAgent: this.userAgent,
    };
  }
}

export class ClientSideNavigatorContextProvider implements NavigatorContext {
  get referrer() {
    return document.referrer;
  }

  get userAgent() {
    return navigator.userAgent;
  }

  get location() {
    return window.location.href;
  }

  get clientId() { 3
    return getCookie(document.cookie, 'coveo_visitorId') || 'MISSING';
  }

  get marshal(): NavigatorContext {
    return {
      clientId: this.clientId,
      location: this.location,
      referrer: this.referrer,
      userAgent: this.userAgent,
    };
  }
}
1 The NavigatorContext interface defines the required properties for the server navigation context provider. The same interface is used for the client navigation context provider later.
2 The clientId getter retrieves the unique visitor ID from a cookie if available; otherwise, it generates and stores a new one. If the user is a returning visitor, it fetches the visitor ID from the coveo_visitorId cookie using getCookieFromRequest() helper. If the cookie doesn’t exist, it checks whether an ID has already been generated in this session. If no ID exists, it generates a new UUID and stores it in this.generatedId for future calls. See Managing client IDs with server-side rendering for more information.
3 The server has already set the coveo_visitorId cookie, so the client can retrieve it using the getCookie() helper.

Define helper methods for working with cookies as illustrated in the following example:

// app/lib/session.ts

export function getCookie(cookieString: string, name: string) { 1
  const cookieName = `${name}=`;

  const cookies = cookieString.split(';');
  for (let i = 0; i < cookies.length; i++) {
    const cookie = cookies[i].trim();

    if (cookie.indexOf(cookieName) === 0) {
      return cookie.substring(cookieName.length, cookie.length);
    }
  }
  return null;
}

export function getCookieFromRequest(request: Request, name: string) { 2
  return getCookie(request.headers.get('Cookie') || '', name);
}
1 The getCookie method extracts the value of a specific cookie by its name from a given cookieString.
2 The getCookieFromRequest method extracts a cookie value from an HTTP request by using the getCookie function.

Headless-specific providers

Once you have the navigation context providers, you can create Headless-specific providers.

Any components that use Headless hooks must be wrapped within a Headless provider.

Coveo Headless React provides the buildProviderWithDefinition utility function to create a provider for a given engine definition.

These providers will be used to wrap your components and provide the necessary context and state to the Headless engine.

// app/lib/providers.tsx
import {buildProviderWithDefinition} from '@coveo/headless-react/ssr-commerce';
import {
  listingEngineDefinition,
  recommendationEngineDefinition,
  searchEngineDefinition,
  standaloneEngineDefinition,
} from './commerce-engine';

export const SearchProvider = buildProviderWithDefinition(
  searchEngineDefinition
);

export const StandaloneProvider = buildProviderWithDefinition(
  standaloneEngineDefinition
);

export const ListingProvider = buildProviderWithDefinition( 1
  listingEngineDefinition
);

export const RecommendationProvider = buildProviderWithDefinition(
  recommendationEngineDefinition
);
1 The ListingProvider wraps product listing page components, providing the listing engine state for SSR hydration.

To use the Headless provider, wrap your components with the provider and provide two required props:

  • navigatorContext: Provide the navigator.

    Since navigatorContext providers have already been created, you can use them to provide the server navigator context to the Headless provider.

  • staticState: Use the pre-fetched state from the server to ensure that the engine has an initial state before rendering.

    To get the static state, define a helper method to fetch the static Headless state.

To understand how this is concretely used when displaying components, see Displaying the search page with the search provider.

Fetch static Headless state

Define the fetchStaticState helper method to fetch the static Headless state. As this method will be invoked by your route’s loader function, it will run on the server to retrieve the static state.

The loader function can then return the static state to the client for hydration.

// app/lib/fetch-static-state.ts

import type {InferStaticState} from '@coveo/headless-react/ssr-commerce';
import type {AppLoadContext} from '@shopify/remix-oxygen';
import {getLocaleFromRequest, mapShopifyCartToCoveoCart} from './cart-utils';
import {engineDefinition, type searchEngineDefinition} from './commerce-engine';

export async function fetchStaticState({
  k,
  query,
  url,
  context,
  request,
}: {
  k:
    | 'listingEngineDefinition'
    | 'searchEngineDefinition'
    | 'standaloneEngineDefinition';
  query: string;
  url: string;
  context: AppLoadContext;
  request: Request;
}) {
  const {country, language, currency} = getLocaleFromRequest(request); 1

  const cart = await context.cart.get();

  return engineDefinition[k].fetchStaticState({ 2
    controllers: {
      parameterManager: {initialState: {parameters: {q: query}}}, 3
      cart: {
        initialState: mapShopifyCartToCoveoCart(cart), 4
      },
      context: { 5
        language: language.toLowerCase(),
        country,
        currency,
        view: {
          url,
        },
      },
    },
  });
}

export type SearchStaticState = InferStaticState<typeof searchEngineDefinition>;
1 Extract the locale information from the request using the custom getLocaleFromRequest helper method. Here, the locale information is hardcoded, but you can extract it from the request.
2 The fetchStaticState method on the Headless engine fetches the static state for the given engine definition, requiring the controllers and their initial states as arguments.
3 The parameterManager controller initializes the search query parameter. For more details on managing parameters, see Managing parameters.
4 The cart controller initializes the cart state using the mapShopifyCartToCoveoCart helper method. For more details on converting the Shopify cart to a Headless cart, see Managing the cart documentation.
5 The context controller initializes the context state with the user’s locale information and the current URL.

Additionally, define a fetchRecommendationStaticState helper method specifically for recommendations.

// app/lib/fetch-recommendation-static-state.ts

import type {AppLoadContext} from '@shopify/remix-oxygen';
import {getLocaleFromRequest, mapShopifyCartToCoveoCart} from './cart-utils';
import {engineDefinition} from './commerce-engine';

export async function fetchRecommendationStaticState({
  k,
  context,
  request,
  productId,
}: {
  context: AppLoadContext;
  request: Request;
  k: 1
  (
    | 'homepageRecommendations'
    | 'cartRecommendations'
    | 'pdpRecommendationsLowerCarousel'
    | 'pdpRecommendationsUpperCarousel'
  )[];
  productId?: string; 2
}) {
  let cartState = {
    items: [] as Array<{
      productId: string;
      name: string;
      price: number;
      quantity: number;
    }>,
  };
  try {
    const cart = await context.cart.get();
    cartState = mapShopifyCartToCoveoCart(cart);
  } catch {
    // Cart fetch may fail if Shopify storefront credentials are not configured.
  }
  const {country, language, currency} = getLocaleFromRequest(request);

  return engineDefinition.recommendationEngineDefinition.fetchStaticState({
    controllers: {
      homepageRecommendations: {
        enabled: k.includes('homepageRecommendations'), 3
        productId,
      },
      cartRecommendations: {
        enabled: k.includes('cartRecommendations'),
        productId,
      },
      pdpRecommendationsLowerCarousel: {
        enabled: k.includes('pdpRecommendationsLowerCarousel'),
        productId,
      },
      pdpRecommendationsUpperCarousel: {
        enabled: k.includes('pdpRecommendationsUpperCarousel'),
        productId,
      },
      cart: {
        initialState: cartState,
      },
      context: {
        language: language.toLowerCase(),
        country,
        currency,
        view: {
          url: request.url, 4
        },
      },
    },
  });
}
1 The values for k are the different types of recommendations that can be fetched.
2 Optional productId parameter to fetch recommendations based on a specific product as seed.
3 Set the enabled property to true if the recommendation type is included in the k array, and false otherwise. This optimization lets you fetch and refresh only the target recommendations.
4 Pass the current request URL so Coveo knows which page the user is on.