Use Coveo Search in the Salesforce Composable Storefront (PWA Kit)
Use Coveo Search in the Salesforce Composable Storefront (PWA Kit)
The Salesforce Composable Storefront (also known as the PWA Kit) is built upon React. Coveo Atomic supports React natively, so it’s the best practice to build a search UI in the Composable Storefront using the Atomic framework.
This article explains the steps required to do so.
Note
Instructions provided in this article apply only if you’re using a Salesforce Commerce Cloud instance (previously known as Demandware). |
Create an app
-
Clone the Composable Storefront repo and go to the folder where you cloned it.
NoteMake sure you’re using Node.js ^14.0.0.
-
To create a demo app, run the following command in your terminal:
npx pwa-kit-create-app
This command will create a demo app for you. During its execution, it will prompt you to choose the Commerce Cloud instance to use in the app. For the sake of simplicity, you can choose
The Retail app with demo Commerce Cloud instance
to use a predefined cloud sandbox. -
Once the app has been created, go to the app folder:
cd pwa-kit-starter-project
Adjust the app to use Coveo
-
To use Atomic in your React project, update the package.json file with the following dependency:
"dependencies": { "@coveo/atomic-react": "^2.0.1", "@coveo/headless": "^2.0.1" }
If this package throws errors while launching the app, you can try to eliminate them by changing the package version to the alpha version.
To get the alpha version number, either visit the Atomic React npm web page, or execute this command in the terminal:
npm view @coveo/atomic | grep alpha
. -
Run
npm install
.NoteIf the installation fails due to some dependencies requiring a more recent version of React, run the following command to upgrade React to that version. For example, if
React@>=18.0.0
is required, run:npm install react@18.0.0 react-dom@18.0.0 --legacy-peer-deps
-
Once all dependencies are installed, move the Atomic localization files to the
static
folder:cp -r node_modules/@coveo/atomic-react/dist/lang app/static
-
To adjust the server side rendering (SSR), open the
app/ssr.js
file and edit thehandler
function:-
Find the directives section and add the following:
'connect-src': ["'self'", "'unsafe-eval'", 'analytics.cloud.coveo.com', 'platform.cloud.coveo.com', 'storage.googleapis.com']
-
Add a route for the default localization file:
app.get('/lang/en.json', runtime.serveStaticFile('static/lang/en.json'))
-
-
To replace the default search, edit the
app/routes.jsx
file.-
In the
Pages
section, add a new constant:const CoveoSearch = loadable(() => import('./pages/coveo-search'), {fallback});
-
In the
Routes
array, find the/search
route and replace it with the following:{ path: '/search', component: CoveoSearch },
-
Build a search UI
For its layouts, the Composable Storefront uses the Chakra UI framework. Combine the Chakra UI tags with the Atomic elements as shown in the code below.
-
Create a
pages/coveo-search/index.jsx
file and paste the following code into it:import React from "react"; import PropTypes from "prop-types"; import { loadFieldActions, loadQueryActions, loadSearchActions, loadSearchAnalyticsActions } from "@coveo/headless"; import { AtomicResultList } from "@coveo/atomic-react"; import { Box, Grid, Stack, Flex, useMultiStyleConfig, AspectRatio, Text, Link } from "@chakra-ui/react"; import DynamicImage from "../../components/dynamic-image"; import { useIntl } from "react-intl"; import { productUrlBuilder } from "../../utils/url"; const CoveoSearch = () => { const intl = useIntl(); const styles = useMultiStyleConfig("ProductTile"); return ( <> <link rel="stylesheet" href="https://static.cloud.coveo.com/atomic/v2/themes/coveo.css" /> <atomic-search-interface language='en'> <Grid templateColumns={{ base: "1fr", md: "280px 1fr" }} columnGap={6}> <Stack display={{ base: "none", md: "flex" }}> <atomic-facet-manager> <atomic-category-facet field="ec_category" label="Category" facet-id="category-facet" delimiting-character="|" ></atomic-category-facet> <atomic-facet field="ec_colors" label="Color" facet-id="color-facet"></atomic-facet> <atomic-numeric-facet field='ec_price' label='Price'></atomic-numeric-facet> <atomic-facet field='year' label='Year'></atomic-facet> <atomic-facet field="ec_brand" label="Brand" facet-id="brand-facet"></atomic-facet> </atomic-facet-manager> </Stack> <Box> <AtomicResultList display='grid' template={(r) => ( <> <Link data-testid='product-tile' {...styles.container} href={productUrlBuilder({ id: r.raw.ec_productid }, intl.local)}> <Box {...styles.imageWrapper}> <AspectRatio {...styles.image}> <DynamicImage src={`${r.raw.ec_images[0]}[?sw={width}&q=60]`} imageProps={{ alt: "image.alt" }} /> </AspectRatio> </Box> {/* Title */} <Text {...styles.title}>{r.raw.ec_name}</Text> {/* Price */} <Text {...styles.price}> { r.raw.ec_price } </Text> </Link> </> )} /> <Flex justifyContent={"center"} paddingTop={8} > <atomic-load-more-results></atomic-load-more-results> </Flex> </Box> </Grid> <atomic-query-error></atomic-query-error> <atomic-no-results></atomic-no-results> </atomic-search-interface> </> ); }; CoveoSearch.getTemplateName = () => "coveo-search"; CoveoSearch.shouldGetProps = ({ previousLocation, location }) => !previousLocation || previousLocation.pathname !== location.pathname || previousLocation.search !== location.search; CoveoSearch.getProps = async ({ engine, location }) => { const searchInterface = document.querySelector("atomic-search-interface"); const urlParams = new URLSearchParams(location.search); let searchQuery = urlParams.get("q"); const newProps = { searchQuery }; if (!engine) { const configuration = { accessToken: "xxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", organizationId: "orgnameplaceholder" }; await searchInterface.initialize(configuration); newProps.engine = engine = searchInterface.engine; const fieldActions = loadFieldActions(engine); engine.dispatch(fieldActions.registerFieldsToInclude( `ec_name, ec_price, ec_images, ec_productid, sf_c_currency, sf_c_image, sf_c_price, title, uri`.split(","))); } // execute search const searchActions = loadSearchActions(engine); const queryActions = loadQueryActions(engine); const analyticsActions = loadSearchAnalyticsActions(engine); await engine.dispatch(queryActions.updateQuery({ q: searchQuery || "test" })); const searchResults = await engine.dispatch(searchActions.executeSearch(analyticsActions.logSearchboxSubmit())); newProps.searchResults = searchResults; return newProps; }; CoveoSearch.propTypes = { engine: PropTypes.object, isLoading: PropTypes.bool, location: PropTypes.object, searchQuery: PropTypes.string, }; export default CoveoSearch;
Imports one component from the atomic-react
package. Use as many components from this package as you want and combine them with pure HTML Atomic tags.Mounts the default Atomic theme. Learn more about Atomic customization in Themes and visual customization. Specifies credentials for the target Coveo organization. See API key and search token authentication. Retrieves the specified fields from the Coveo organization. These fields are used throughout the UI layout, for example, in facets. To see the search results, make sure that your Coveo organization already has data indexed from a Salesforce Commerce Cloud instance.
-
To launch the application locally, run the
npm start
command. The application will open in the browser, at thehttp://localhost:3000
address by default. -
In the upper-right corner, you can see the search bar. Try to type in "dress" or "shirt" and then select Enter to see the search results powered by Coveo.