Headless commerce usage (server-side rendering)

This is for:

Developer

Server-side rendering (SSR) is a technique that allows web applications to be rendered 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.

The @coveo/headless-react package includes utilities for React which are compatible with Next.js in the @coveo/headless-react/ssr-commerce sub-package. These utilities enable SSR with the Headless framework.

This article introduces the different pieces required to put together a Headless SSR commerce app using a Next.js example. It assumes you already understand how to set up a Next.js project.

Note

While the core concepts discussed in this article apply to SSR with React, the code samples are specific to Next.js.

At a high level, here’s the architecture involved:

SSR diagram
Tip

To focus on relevant concepts and ease comprehension, the code samples in this article are simplified. For a complete example, see the headless-ssr-commerce sample. Links to different parts of the sample will be provided throughout this article.

Install the library

Use npm to install Headless React Utils for SSR:

npm install @coveo/headless-react

Define the commerce engine and controllers

The Headless commerce engine manages the state of the product discovery solution interface and communicates with the Coveo Platform. Rather than initializing the commerce engine in the client, you must define a commerce engine. This is because the commerce engine definition is a singleton that must be shared between the server and the client.

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

When defining your engine, specify the controllers you want to use in your commerce site. You’ll then be able to retrieve controller hooks to interact with the engine in your components.

The following is an example commerce engine configuration file.

// lib/commerce-engine-config.ts

import {1
  CommerceEngineDefinitionOptions,
  defineContext,
  defineSummary,
  defineRecommendations,
  // ...
} from '@coveo/headless-react/ssr-commerce';

export default {
  configuration: {
    organizationId: 'searchuisamples', 2
    accessToken: 'xx564559b1-0045-48e1-953c-3addd1ee4457', 3
    context: { 4
      language: 'en',
      country: 'US',
      currency: 'USD',
      view: {
        url: 'https://sports.barca.group',
      },
    },
    analytics: { 5
      trackingId: 'sports-ui-samples',
    },
    cart: { 6
      items: [
        {
          // ...
        },
      ],
    },
  },
  controllers: { 7
    context: defineContext(),
    summary: defineSummary(),
    popularViewed: defineRecommendations({ 8
      options: {
        slotId: 'd73afbd2-8521-4ee6-a9b8-31f064721e73',
      },
    }),
    // ...
  },
} satisfies CommerceEngineDefinitionOptions;
1 Import the necessary controller definers.
2 The organization ID is the unique identifier of your Coveo organization.
3 The access token is a search token or an API key that grants the Allowed access level on the Execute Queries domain and the Push access level on the Analytics Data domain in the target organization.

To improve security, client-side specification of user IDs isn’t supported by Event Protocol. To specify user IDs, enforce them through search tokens.

4 The context object contains information about the user’s context, such as the currency, country, language, and the URL.

Every time the user navigates between pages, you must update this context.

5 Via the analytics object, specify the tracking ID.
6 Pass in the initial state of the cart by specifying the CartInitialState object.
7 Specify the controllers to define as part of the commerce engine definition.
8 Certain controller definers require additional options. For example, here you need to specify which recommendation slot to use.

Use your engine configuration to define the commerce engine, as in the following example.

// 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, 2
  searchEngineDefinition,
  recommendationEngineDefinition,
  standaloneEngineDefinition,
  useEngine, 3
} = engineDefinition;

export const {
  useContext,
  useSummary,
  usePopularViewed,
  // ...
} = engineDefinition.controllers; 4
1 Define the commerce engine using the target commerce engine configuration.
2 Engines for different use cases in your commerce site. More on that later.
3 useEngine is a hook that lets you access the core commerce engine. More on that later.
4 Retrieve the controller hooks created based on the controllers definitions you provided in the commerce engine configuration.

For complete code samples, see commerce-engine-config.ts and commerce-engine.ts.

Create a navigation context provider

For SSR, you must create a navigation context provider that will wrap relevant navigation context information (such as the URL and clientId) and make it available to the server.

Capture the navigation context with middleware

Create a Next.js middleware to handle the URL and client ID. For simplicity, this example always generates a new clientId for each request. In a real implementation, you would likely use a more sophisticated approach to generate and manage client IDs with browser cookies or local storage. See Managing client IDs with server-side rendering.

// middleware.ts 1

import {NextRequest, NextResponse} from 'next/server';

export default function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const requestHeaders = new Headers(request.headers);
  const uuid = crypto.randomUUID(); 2
  requestHeaders.set('x-coveo-client-id', uuid); 3
  response.headers.set('x-coveo-client-id', uuid);
  response.headers.set('x-href', request.nextUrl.href); 4
  return response;
}
1 Next.js treats middleware.ts as a middleware file.
2 Generate a unique clientId for the request and response.
3 Set the clientId in both the request and response headers so that it can be used by the commerce engine server-side and client-side across sessions.
4 Make sure to pass along the x-href header, which is the URL of the request, in the response. More on that below.

Create the navigation context object

Then, in your Next.js app, create a navigation context provider that will use the headers from the middleware to create a navigation context object.

// lib/navigatorContextProvider.ts

import {NavigatorContext} from '@coveo/headless-react/ssr-commerce';
import type {ReadonlyHeaders} from 'next/dist/server/web/spec-extension/adapters/headers';

export class NextJsNavigatorContext implements NavigatorContext {
  constructor(private headers: ReadonlyHeaders) {} 1

  get referrer() {
    return this.headers.get('referer') || this.headers.get('referrer'); 2
  }

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

  get location() {
    return this.headers.get('x-href');
  }

  get clientId() {
    const clientId = this.headers.get('x-coveo-client-id');
    return clientId!;
  }

  get marshal(): NavigatorContext { 3
    return {
      clientId: this.clientId,
      location: this.location,
      referrer: this.referrer,
      userAgent: this.userAgent,
    };
  }
}
1 Takes in the read-only headers from a Next.js request, which let you access request-specific data.
2 Some browsers use referer while others may use referrer.
3 Marshals the navigation context into a format that can be used by Coveo’s Headless library. See NavigatorContext for more information.

You’ll use your context provider later in the different pages of your site (more details on creating pages later).

See navigatorContextProvider.ts and page.tsx for complete code samples.

Create providers

Providers take care of displaying your page with the static state, and then hydrating the state and displaying the page with the hydrated state. They are required for your controller hooks to function.

SSR diagram

Coveo Headless React exposes the buildProviderWithDefinition utility function that creates a provider for a given engine definition. It simplifies the process of building providers for your components.

// components/providers/providers.tsx

'use client';

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

export const ListingProvider = buildProviderWithDefinition( 1
  listingEngineDefinition
);

export const SearchProvider = buildProviderWithDefinition( 2
  searchEngineDefinition
);

export const RecommendationProvider = buildProviderWithDefinition( 3
  recommendationEngineDefinition
);

export const StandaloneProvider = buildProviderWithDefinition( 4
  standaloneEngineDefinition
);
1 For listing pages to provide context for listing-specific hooks.
2 For search pages to provide context for search-specific hooks.
3 For recommendation slots, regardless of the slot location (for example, home page, product detail page, or some other page.)
4 For components that don’t require triggering a search or product fetch (such as a cart page or standalone search box).

Create components

In a Headless SSR commerce implementation, contrary to a non-SSR one, you generally don’t interact with the controllers or engine directly in your components. We rather recommend leveraging the controller and engine hooks retrieved earlier.

For example, here’s how to implement a ShowMore component.

// components/show-more.tsx

'use client'; 1

import {usePagination, useSummary} from '@/lib/commerce-engine'; 2

export default function ShowMore() {
  const pagination = usePagination();
  const summary = useSummary();

  const isDisabled = () => { 3
    return (
      !pagination.methods ||
      summary.state?.lastProduct === summary.state?.totalNumberOfProducts
    );
  };

  return (
    <>
      <div>
        Displaying {summary.state?.lastProduct ?? pagination.state.pageSize} out of{' '}
        {pagination.state.totalEntries} products
      </div>
      <button
        className="ShowMore"
        disabled={isDisabled()}
        onClick={pagination.methods?.fetchMoreProducts}
      >
        Show more
      </button>
    </>
  );
}
1 Runs client-side.
2 Import the relevant controller hooks from the commerce engine.
3 Use the hooks to access the state and methods of the controllers.

For more samples, see components.

Create a component that lets users change context

In a Coveo commerce SSR setup, you might want to create a component that lets users change the context of the engine. For example, you might need to let users change the language, country and currency displayed on your site. You can do so with the useEngine hook and a hook on the Context controller.

// components/context-dropdown.tsx

'use client';

import {useContext, useEngine} from '@/lib/commerce-engine';
import {
  CommerceEngine,
  ContextOptions,
  loadProductListingActions,
  loadSearchActions,
} from '@coveo/headless-react/ssr-commerce';

const storefrontAssociations = [ 1
  'en-CA-CAD',
  'fr-CA-CAD',
  'en-GB-GBP',
  'en-US-USD',
];

export default function ContextDropdown({
  useCase,
}: {
  useCase?: 'listing' | 'search';
}) {
  const context = useContext();
  const engine = useEngine();

  return (
    <div>
      <p></p>
      Context dropdown :
      <select
        value={`${context.state.language}-${context.state.country}-${context.state.currency}`} 2
        onChange={(e) => {
          const [language, country, currency] = e.target.value.split('-');
          context.methods?.setLanguage(language); 3
          context.methods?.setCountry(country);
          context.methods?.setCurrency(currency as ContextOptions['currency']);

          useCase === 'search' 4
            ? engine?.dispatch( 5
                loadSearchActions(engine as CommerceEngine).executeSearch()
              )
            : useCase === 'listing' &&
              engine?.dispatch(
                loadProductListingActions(
                  engine as CommerceEngine
                ).fetchProductListing()
              );
        }}
      >
        {storefrontAssociations.map((association) => (
          <option key={association} value={association}>
            {association}
          </option>
        ))}
      </select>
      <p></p>
    </div>
  );
}
1 A hardcoded list of storefront associations for switching app context by language, country, and currency, for demonstration purposes. In a real application, these values would likely come from sources like environment variables or an API.
2 Use the context controller hook to access the context state.
3 Similarly, use the context controller hook to access the context methods.
4 Check the useCase prop to determine whether the component is being used for search or listing.
5 After adjusting the content, dispatch an action that updates the content by triggering a query.

For a complete sample, see context-dropdown.tsx.

Assemble pages

Once you’ve created your providers and the components you want to include in your site, assemble your pages using them.

Select the appropriate engine and provider

When doing so, use the target engine definition and the appropriate provider to wrap your components:

Use case Engine definition Provider

Search

searchEngineDefinition

SearchProvider

Listing

listingEngineDefinition

ListingProvider

Recommendations

recommendationEngineDefinition

RecommendationProvider

Standalone search box

standaloneEngineDefinition

StandaloneProvider

Cart page

standaloneEngineDefinition

StandaloneProvider

If you use an incompatible engine definition or provider for a given use case, you may encounter errors, missing properties, or unexpected behavior.

Example

The following is a simplified search page example, integrating the different pieces you’ve created. For more complete instructions, see Build your search interface: Server-side rendering.

// app/search/page.tsx

// ...
import ShowMore from '@/components/show-more';
import {SearchProvider} from '@/components/providers/providers';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {headers} from 'next/headers';

export default async function Search() {
  const navigatorContext = new NextJsNavigatorContext(headers()); 1
  searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext);

  const items = {
    // ... 2
  };

  const staticState = await searchEngineDefinition.fetchStaticState({ 3
    controllers: {
      cart: {initialState: items},
      context: {
        language: 'en',
        country: 'US',
        currency: 'USD',
        view: {
          url: 'https://sports.barca.group/search',
        },
      },
    },
  });

  return (
    <SearchProvider 4
      staticState={staticState} 5
      navigatorContext={navigatorContext.marshal}
    >
      <ContextDropdown useCase="search" /> 6
      <!--- ... --->
      <ShowMore />
    </SearchProvider>
  );
}

export const dynamic = 'force-dynamic'; 7
1 As discussed earlier, use your navigation context provider to set the navigator context before fetching the app static state. Below, you’ll pass the navigator context to the SearchProvider component in the return value.
2 Retrieve cart items, because you’ll need them when fetching the static state. The details are omitted here for brevity. In a real implementation, you would use your own logic to fetch the cart items, depending on your commerce site setup.
3 Fetches the static state of the app with initial state. Note that you need to pass initial values for the cart and context controllers, so the static state can be generated with the target initial state.
4 Wrap your search page components with the SearchProvider component.
5 Pass the static state and navigator context to the SearchProvider component, which wraps all components in your page.
6 Include the target components.
7 Force the server to always dynamically generate the page, based on fresh context.

What’s next?

For more samples, see the UI-KIT repository.

See especially the following, which exemplify the essential components of a Coveo Commerce SSR implementation: