Getting started (Shopify Hydrogen)
Getting started (Shopify Hydrogen)
|
|
Shopify Hydrogen versions 2024.01 and later use the Don’t use the latest Shopify Hydrogen CLI or the current Shopify Hydrogen getting-started guide, as they scaffold a 2024+ project. Hydrogen 2023.10.x requires Node.js 18. This guide uses |
This article introduces key concepts for using the Coveo Headless library with a Shopify Hydrogen storefront. It includes code samples to help you set up a standalone search box where you can enter queries and receive suggestions, and a search page that displays product results.
Some functionalities have been omitted to keep the example short. For more detailed information, see the related links in each section.
Prerequisites
You must have a Shopify Hydrogen storefront set up and running on version 2023.10.x or earlier.
The versioned scaffold CLI for Hydrogen 2023.10 is no longer available on npm, so you can’t use create-hydrogen to generate a compatible project.
Instead, use the hello-world template from the 2023.10.6 release on GitHub as a starting point.
Your Hydrogen project must also include a SESSION_SECRET environment variable.
Create a .env file in the project root:
SESSION_SECRET="your-secret-value"
PUBLIC_STORE_DOMAIN="your-store.myshopify.com"
PUBLIC_STOREFRONT_API_TOKEN="your-public-storefront-api-token"
PRIVATE_STOREFRONT_API_TOKEN="your-private-storefront-api-token"
PUBLIC_STOREFRONT_ID="your-storefront-id"
|
|
Hydrogen’s default Content Security Policy (CSP) blocks requests to external domains.
To allow Coveo API calls, update the
|
Install Headless
Use npm to install the Headless React package.
npm install @coveo/headless-react@2.9.13
Define the commerce engine and controllers
Create an engine configuration file to specify how Headless communicates with Coveo. Define the commerce engine configuration details and specify the controllers to use in your commerce interface.
// app/lib/commerce-engine-config.ts
import {
type CommerceEngineDefinitionOptions,
defineBreadcrumbManager,
defineCart,
defineContext,
defineFacetGenerator,
defineInstantProducts,
definePagination,
defineParameterManager,
defineProductList,
defineRecommendations,
defineSearchBox,
defineSort,
defineStandaloneSearchBox,
defineSummary,
getSampleCommerceEngineConfiguration,
} from '@coveo/headless-react/ssr-commerce';
import {
CART_SLOT_ID,
HOMEPAGE_SLOT_ID,
PDP_LOWER_CAROUSEL_SLOT_ID,
PDP_UPPER_CAROUSEL_SLOT_ID,
} from './recommendation-slots';
export default {
configuration: {
...getSampleCommerceEngineConfiguration(),
},
controllers: {
context: defineContext(),
cart: defineCart(),
summary: defineSummary(),
searchBox: defineSearchBox(),
standaloneSearchBox: defineStandaloneSearchBox({
options: {redirectionUrl: '/search', id: 'standalone-search-box'},
}),
instantProducts: defineInstantProducts(),
productList: defineProductList(),
parameterManager: defineParameterManager(),
facetGenerator: defineFacetGenerator(),
sort: defineSort(),
pagination: definePagination(),
breadcrumbManager: defineBreadcrumbManager(),
homepageRecommendations: defineRecommendations({
options: {slotId: HOMEPAGE_SLOT_ID},
}),
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;
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);
export const {
listingEngineDefinition,
searchEngineDefinition,
recommendationEngineDefinition,
standaloneEngineDefinition,
useEngine,
} = engineDefinition;
export const {
useContext,
useCart,
useSummary,
useSearchBox,
useProductList,
useParameterManager,
useStandaloneSearchBox,
useInstantProducts,
useFacetGenerator,
useSort,
usePagination,
useBreadcrumbManager,
useHomepageRecommendations,
useCartRecommendations,
usePdpRecommendationsLowerCarousel,
usePdpRecommendationsUpperCarousel,
} = engineDefinition.controllers;
export {fetchStaticState} from './fetch-static-state-simple'; 
| Define the commerce engine using the target commerce engine configuration. | |
| Engines for different use cases in your commerce site. These engine definitions will be imported in your components to access the engine state. | |
| Retrieve the controller hooks created based on the controllers definitions you provided in the commerce engine configuration. | |
Re-export fetchStaticState so that route files can import everything from a single module. |
For more details on how engines and controllers work, see Core concepts: Headless engine and controllers.
Using providers
Providers establish the context and manage state for a Coveo Headless engine within a Hydrogen application.
To enable server-side rendering (SSR) with Coveo Headless and Hydrogen, you must create two types of providers:
-
Navigation context providers
-
Headless-specific providers
For more details on how providers are used, see Core concepts: Using 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 {
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() {
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() {
return getCookie(document.cookie, 'coveo_visitorId') || 'MISSING';
}
get marshal(): NavigatorContext {
return {
clientId: this.clientId,
location: this.location,
referrer: this.referrer,
userAgent: this.userAgent,
};
}
}
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. |
|
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. |
|
The server has already set the coveo_visitorId cookie, so the client can retrieve it using the getCookie() helper. |
Helper cookie methods
Define helper methods for working with cookies as illustrated in the following example:
// app/lib/session.ts
export function getCookie(cookieString: string, name: string) {
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) {
return getCookie(request.headers.get('Cookie') || '', name);
}
The getCookie method extracts the value of a specific cookie by its name from a given cookieString. |
|
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(
listingEngineDefinition
);
export const RecommendationProvider = buildProviderWithDefinition(
recommendationEngineDefinition
);
The ListingProvider wraps product listing page components, providing the listing engine state for SSR hydration. |
Fetch static Headless state
Define a helper function to fetch the static state of the Headless engine. More on how it’s used later.
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-simple.ts
import type {
buildParameterSerializer,
InferStaticState,
} from '@coveo/headless-react/ssr-commerce';
import {
engineDefinition,
type standaloneEngineDefinition,
} from './commerce-engine';
type CommerceParameters = ReturnType<
ReturnType<typeof buildParameterSerializer>['deserialize']
>;
function getLocale() {
const country = 'US';
const language = 'en';
const currency = 'USD';
return {country, language, currency} as const;
}
export async function fetchStaticState({
k,
url,
q,
parameters,
}: {
k:
| 'listingEngineDefinition'
| 'searchEngineDefinition'
| 'standaloneEngineDefinition';
url: string;
q?: string;
parameters?: CommerceParameters;
}) {
const {country, language, currency} = getLocale();
return engineDefinition[k].fetchStaticState({
controllers: {
context: {
language: language.toLowerCase(),
country,
currency,
view: {
url,
},
},
cart: {initialState: {items: []}},
parameterManager: {
initialState: {parameters: {q: q || '', ...parameters}},
},
},
});
}
export type StandaloneStaticState = InferStaticState<
typeof standaloneEngineDefinition
>;
Pass the current request URL. The context.view.url tells Coveo which page the user is on. |
|
| Extract the locale information. Here the locale is hardcoded, but in production you would extract it from the request. | |
The fetchStaticState method fetches the static state for the given engine definition using the provided controllers as the basis for initial state configuration. In a real application, you would also specify the initial state for other controllers, such as the cart controller. For more details, see Core concepts: Fetch static Headless state. |
Build a standalone search box component
Now that you have the engine and providers set up, you can build a standalone search box component using the Headless library. This component is intended for pages that don’t display search results (for example, a home page) and redirects users to the search page on submit.
The following example shows how to build a standalone search box component using the useStandaloneSearchBox hook.
The component features a search input field, a submit button, and a dynamic list of query suggestions.
// app/components/StandaloneSearchBox.tsx
import {useNavigate} from '@remix-run/react';
import {useEffect, useRef} from 'react';
import {useStandaloneSearchBox} from '~/lib/commerce-engine';
export function StandaloneSearchBox() {
const searchBox = useStandaloneSearchBox();
const inputRef = useRef<HTMLInputElement>(null);
const navigate = useNavigate();
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
if (searchBox.state.redirectTo === '/search') {
navigate(
`${searchBox.state.redirectTo}?q=${encodeURIComponent(
searchBox.state.value
)}`
);
}
}, [searchBox.state.redirectTo, searchBox.state.value, navigate]);
const handleSuggestionClick = (suggestion: string) => {
searchBox.methods?.updateText(suggestion);
inputRef.current!.value = suggestion;
searchBox.methods?.showSuggestions();
};
return (
<div>
<input
ref={inputRef}
aria-label="Search"
placeholder="Search"
onChange={(e) => searchBox.methods?.updateText(e.target.value)}
onFocus={() => searchBox.methods?.showSuggestions()}
/>
<button type="button" onClick={searchBox.methods?.submit}>
Search
</button>
{searchBox.state.suggestions.length > 0 && (
<div>
{searchBox.state.suggestions.map((suggestion) => (
<button
type="button"
key={suggestion.rawValue}
onClick={() => handleSuggestionClick(suggestion.rawValue)}
>
{suggestion.highlightedValue}
</button>
))}
</div>
)}
</div>
);
}
For a more detailed explanation of how the search box component works, see Build search interfaces.
Display the standalone search box
Now that you have the search box component, you can display it in your Hydrogen storefront.
The following route file shows how to wrap the StandaloneSearchBox with a StandaloneProvider and wire up the server-side static state:
import type {InferStaticState} from '@coveo/headless-react/ssr-commerce';
import {useLoaderData} from '@remix-run/react';
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {StandaloneSearchBox} from '~/components/StandaloneSearchBox';
import {
fetchStaticState,
standaloneEngineDefinition,
} from '~/lib/commerce-engine';
import {
ClientSideNavigatorContextProvider,
ServerSideNavigatorContextProvider,
} from '~/lib/navigator-provider';
import {StandaloneProvider} from '~/lib/providers';
type StandaloneStaticState = InferStaticState<
typeof standaloneEngineDefinition
>;
export async function loader({request}: LoaderFunctionArgs) {
standaloneEngineDefinition.setNavigatorContextProvider(
() => new ServerSideNavigatorContextProvider(request)
);
const standaloneStaticState = await fetchStaticState({
k: 'standaloneEngineDefinition',
url: request.url,
});
return {standaloneStaticState};
}
export default function Homepage() {
const {standaloneStaticState} = useLoaderData<typeof loader>();
return (
<div>
<h2>Welcome</h2>
<p>
This is an example Shopify Hydrogen storefront with Coveo Headless
commerce integration.
</p>
<StandaloneProvider
navigatorContext={new ClientSideNavigatorContextProvider()}
staticState={standaloneStaticState as StandaloneStaticState}
>
<StandaloneSearchBox />
</StandaloneProvider>
</div>
);
}
| The loader runs on the server to fetch the static state for the standalone search box before rendering. | |
| Pass the current request URL so Coveo knows which page the user is on. | |
Wrap your components with the StandaloneProvider and provide the navigatorContext and staticState props. |
|
Since navigation context providers have already been created, use ClientSideNavigatorContextProvider on the client side. |
|
| Use the pre-fetched static state from the loader to hydrate the engine on the client without an additional request. |
Display search results
You can also build a dedicated search page that displays product results using the SearchProvider with the useProductList, useSummary, and useSearchBox hooks:
// app/routes/search.tsx
import type {InferStaticState} from '@coveo/headless-react/ssr-commerce';
import {buildParameterSerializer} from '@coveo/headless-react/ssr-commerce';
import {type MetaFunction, useLoaderData, useLocation} from '@remix-run/react';
import type {LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {Facets} from '~/components/Facets';
import {Pagination} from '~/components/Pagination';
import ParameterManager from '~/components/ParameterManager';
import {ProductList} from '~/components/ProductList';
import {SearchBox} from '~/components/SearchBox';
import {Sort} from '~/components/Sort';
import {
fetchStaticState,
searchEngineDefinition,
standaloneEngineDefinition,
useSummary,
} from '~/lib/commerce-engine';
import {
ClientSideNavigatorContextProvider,
ServerSideNavigatorContextProvider,
} from '~/lib/navigator-provider';
import {SearchProvider} from '~/lib/providers';
type SearchStaticState = InferStaticState<typeof searchEngineDefinition>;
export const meta: MetaFunction = () => {
return [{title: 'Coveo | Search'}];
};
export async function loader({request}: LoaderFunctionArgs) {
const navigatorContextProvider = () =>
new ServerSideNavigatorContextProvider(request);
searchEngineDefinition.setNavigatorContextProvider(navigatorContextProvider);
standaloneEngineDefinition.setNavigatorContextProvider(
navigatorContextProvider
);
const url = new URL(request.url);
const {deserialize} = buildParameterSerializer();
const parameters = deserialize(url.searchParams);
const query = url.searchParams.get('q') || '';
const searchStaticState = await fetchStaticState({
k: 'searchEngineDefinition',
url: request.url,
parameters,
q: query || undefined,
});
return {searchStaticState, query};
}
function SearchSummary() {
const summary = useSummary();
const {firstProduct, lastProduct, totalNumberOfProducts} = summary.state;
const hasResults =
'hasResults' in summary.state
? summary.state.hasResults
: totalNumberOfProducts > 0;
const query = 'query' in summary.state ? summary.state.query : '';
if (!hasResults) return null;
return (
<p>
Showing {firstProduct}-{lastProduct} of {totalNumberOfProducts} results
{query ? ` for "${query}"` : ''}
</p>
);
}
export default function SearchPage() {
const {searchStaticState} = useLoaderData<typeof loader>();
const location = useLocation();
const currentUrl = `${location.pathname}${location.search}${location.hash}`;
return (
<SearchProvider
navigatorContext={new ClientSideNavigatorContextProvider()}
staticState={searchStaticState as SearchStaticState}
>
<ParameterManager url={currentUrl} />
<SearchBox />
<SearchSummary />
<Sort />
<Facets />
<ProductList />
<Pagination />
</SearchProvider>
);
}
| Use the loader function to run server-side code. | |
| Set the navigator context provider to the server-side context provider. | |
| Fetch the deserialized search parameters from the URL. | |
| Fetch the static state for the search page. | |
Wrap the search page with the SearchProvider component. |
Next steps
Now that you’ve seen how to set up the Headless library in a Hydrogen storefront with a working search page, you can extend the example with additional controllers and components.
For more details on building search pages and other commerce interfaces, see Build search interfaces.
To learn more about the core concepts in this article, see Core concepts.