Use Coveo Search in the Salesforce Composable Storefront (PWA Kit)

This is for:

Developer

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.

Create an app

  1. Clone the Composable Storefront repoand go to the folder where you cloned it.

    Note

    Make sure you’re using Node.js ^14.0.0.

  2. 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.

  3. Once the app has been created, go to the app folder:

    cd pwa-kit-starter-project

Adjust the app to use Coveo

  1. 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"
    }
    Important

    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.

  2. Run npm install.

    Note

    If 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
  3. 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
  4. To adjust the server side rendering (SSR), open the app/ssr.js file and edit the handler function:

    1. Find the directives section and add the following:

      'connect-src': ["'self'", "'unsafe-eval'", 'analytics.cloud.coveo.com', 'platform.cloud.coveo.com', 'storage.googleapis.com']
    2. Add a route for the default localization file:

      app.get('/lang/en.json', runtime.serveStaticFile('static/lang/en.json'))
  5. To replace the default search, edit the app/routes.jsx file.

    1. In the Pages section, add a new constant:

      const CoveoSearch = loadable(() => import('./pages/coveo-search'), {fallback});
    2. 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.

  1. 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"; 1
    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"
          /> 2
          <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"
        }; 3
        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(",")));
      } 4
    
      // 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;
    1 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.
    2 Mounts the default Atomic theme. Learn more about Atomic customization in Themes and visual customization.
    3 Specifies credentials for the target Coveo organization. See API key and search token authentication.
    4 Retrieves the specified fields from the Coveo organization. These fields are used throughout the UI layout, for example, in facets.
    Important

    To see the search results, make sure that your Coveo organization already has data indexed from a Salesforce Commerce Cloud instance.

  2. To launch the application locally, run the npm start command. The application will open in the browser, at the http://localhost:3000 address by default.

  3. 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.