Build search interfaces (SSR)

This is for:

Developer
Important

The Headless Commerce SSR utilities are in open beta. Contact your Coveo representative for support in adopting this.

Building a server-side rendered (SSR) search interface with Coveo Headless involves three main components: a search box, a search page, and a search provider.

The Coveo Headless React package provides two types of search box controllers: the SearchBox and the StandaloneSearchBox. It also exposes functions to create a search provider to manage the search page and results.

  • SearchBox: Use this controller to create a search box component in your search interface, submit queries, and display query suggestions. This controller is used on the search page itself.

  • StandaloneSearchBox: Use this controller to create a standalone search box that redirects to your search page. This controller is used on every page of your app except the search page.

  • Search provider: Use this provider to manage search results, such as facets, sorting, and pagination, ensuring a seamless user experience.

Complete examples are available in the Headless repository.

Prerequisites

Before you begin building your search interface, make sure you:

To create a search box, use the SearchBox and StandaloneSearchBox controllers.

Using the SearchBox controller

When defining your commerce engine, specify the SearchBox controller and retrieve the corresponding hook, as follows:

// lib/commerce-engine-config.ts

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

export default {
  // ...
  controllers: {
    searchBox: defineSearchBox(),
    // ...
  },
} satisfies CommerceEngineDefinitionOptions;

Next, create a search box component that uses the SearchBox controller hook.

// components/Search-box.tsx

'use client';

import {
  useSearchBox,
} from '@/lib/commerce-engine';

export default function SearchBox() {
  const searchBox = useSearchBox(); 1

  const onSearchBoxInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    searchBox.methods?.updateText(e.target.value); 2
  };

  return (
    <div>
      <input
        type="search"
        aria-label="searchbox"
        placeholder="search"
        value={searchBox.state.value}
        onChange={(e) => onSearchBoxInputChange(e)}
      ></input>
      {searchBox.state.value !== '' && (
        <span>
          <button onClick={searchBox.methods?.clear}>X</button>
        </span>
      )}
      <button onClick={searchBox.methods?.submit}>Search</button>
      {searchBox.state.suggestions.length > 0 && ( 3
        <ul>
          Suggestions :
          {searchBox.state.suggestions.map((suggestion, index) => (
            <li key={index}>
              <button
                onClick={() =>
                  searchBox.methods?.selectSuggestion(suggestion.rawValue)
                }
                dangerouslySetInnerHTML={{
                  __html: suggestion.highlightedValue,
                }}
              ></button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
1 Retrieve the SearchBox controller hook.
2 Update the query when the user types in the search box by calling the updateText method on the SearchBox controller hook.
3 Display query suggestions when the user types in the search box, if suggestions are available. When the user clicks a suggestion, call the selectSuggestion method on the SearchBox controller hook to submit the selected suggestion as a query.

This component can now be included in your search page.

Using the StandaloneSearchBox controller

In addition to creating a search box component, create a lightweight standalone search box component that utilizes the StandaloneSearchBox controller to redirect to your search page. This standalone search box component should be included on every page of your app except the search page.

When defining your commerce engine, specify the StandaloneSearchBox controller and retrieve the corresponding hook.

// lib/commerce-engine-config.ts

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

export default {
  // ...
  controllers: {
    standaloneSearchBox: defineStandaloneSearchBox({1
      options: {redirectionUrl: '/search'},
    }),
    // ...
  },
} satisfies CommerceEngineDefinitionOptions;
1 Define the StandaloneSearchBox controller hook by passing the redirection URL to the search page.

When the user submits a query in this search box, the state.redirectTo is updated to /search. This update can be detected by subscribing to the controller state and then used to redirect the user to the search page.

Next, create a component that uses this StandaloneSearchBox controller. It looks similar to the search box component, with the addition of redirection logic.

'use client';

import {
  useStandaloneSearchBox,
} from '@/lib/commerce-engine';
import {useRouter} from 'next/navigation'; 1
import {useEffect} from 'react';

export default function StandaloneSearchBox() {
  const standaloneSearchBox = useStandaloneSearchBox();

  const router = useRouter();

  useEffect(() => { 2
    if (standaloneSearchBox.state.redirectTo === '/search') {
      const url = `${standaloneSearchBox.state.redirectTo}#q=${encodeURIComponent(standaloneSearchBox.state.value)}`;
      router.push(url, {scroll: false});
      standaloneSearchBox.methods?.afterRedirection(); 3
    }
  }, [standaloneSearchBox.state.redirectTo, standaloneSearchBox.state.value, router, standaloneSearchBox.methods]);

  const onSearchBoxInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    standaloneSearchBox.methods?.updateText(e.target.value);
  };

  return (
    <div>
      <input
        type="search"
        aria-label="searchbox"
        placeholder="search"
        value={standaloneSearchBox.state.value}
        onChange={(e) => onSearchBoxInputChange(e)}
      ></input>
      {standaloneSearchBox.state.value !== '' && (
        <span>
          <button onClick={standaloneSearchBox.methods?.clear}>X</button>
        </span>
      )}
      <button onClick={() => standaloneSearchBox.methods?.submit()}>Search</button>

      {standaloneSearchBox.state.suggestions.length > 0 && (
        <ul>
          Suggestions :
          {standaloneSearchBox.state.suggestions.map((suggestion, index) => (
            <li key={index}>
              <button
                onClick={() =>
                  standaloneSearchBox.methods?.selectSuggestion(suggestion.rawValue)
                }
                dangerouslySetInnerHTML={{
                  __html: suggestion.highlightedValue,
                }}
              ></button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
1 The Next.js useRouter hook is used to access the router object and navigate to the search page.
2 Redirect to the search page when the user submits a query and the redirectTo property of the controller is set to /search.
3 This method resets the state after the redirection.

A complete example, including a sample implementation of the standalone search box component, is available in the Headless repository.

Displaying the search page with the search provider

In addition to rendering the search box, you need to display the search results on the search page, using your search provider.

// app/search/page.tsx

import ContextDropdown from '@/components/context-dropdown';
import FacetGenerator from '@/components/facets/facet-generator';
import ParameterManager from '@/components/parameter-manager';
import ProductList from '@/components/product-list';
import {SearchProvider} from '@/components/providers/providers';
import SearchBox from '@/components/search-box';
import {searchEngineDefinition} from '@/lib/commerce-engine';
import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider';
import {defaultContext} from '@/utils/context';
import {buildParameterSerializer} from '@coveo/headless-react/ssr-commerce';
import {headers} from 'next/headers';
import Pagination from '@/components/pagination';

export default async function Search({
  searchParams, 1
}: {
  searchParams: Promise<URLSearchParams>;
}) {
  const navigatorContext = new NextJsNavigatorContext(headers()); 2
  searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext);

  const {deserialize} = buildParameterSerializer(); 3
  const parameters = deserialize(await searchParams);

  const items = // ... 4

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

  return (
    <SearchProvider 5
      staticState={staticState}
      navigatorContext={navigatorContext.marshal}
    >
      <ParameterManager url={navigatorContext.location} /> 6
      <ContextDropdown useCase="search" /> 7
      <div style={{display: 'flex', flexDirection: 'row'}}>
        <div style={{flex: 1}}>
          <FacetGenerator /> 8
        </div>
        <div style={{flex: 2}}>
          <SearchBox /> 9
          <ProductList /> 10
          <Pagination></Pagination> 11
        </div>
      </div>
    </SearchProvider>
  );
}

export const dynamic = 'force-dynamic';
1 Retrieve the search parameters from the URL.
2 Create a navigation context provider.
3 Deserialize the search parameters.
4 Retrieve cart items.
5 Wrap your components with your search provider.
6 Use your parameter manager component.
7 Let your users change context.
8 Display the facets generated by the FacetGenerator controller.
9 Display the sorting options generated by the Sort controller.
10 Render the list of products returned by the search.
11 Display the pagination generated by the Pagination controller.

A complete example of how to build a search page is available in the Headless repository.

Within the sample project, you can find additional components that were omitted in this article, such as the BreadcrumbManager (displays a summary of the currently active facet values) and Summary (provides a summary of search results such as the number of results returned).