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

The Salesforce Composable Storefront (also known as the PWA Kit) is built upon React, which Coveo Atomic supports natively.

This article explains the steps required to build a basic search UI in the Composable Storefront using the Atomic framework.

Create a Demo App

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

  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 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": "^1.4.15"
    }
    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.

  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. You can combine the Chakra UI tags with 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/atomic/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 2
            rel="stylesheet"
            href="https://static.cloud.coveo.com/atomic/v1/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", 3
          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(",")));
      } 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. You can 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.
    4 Retrieves the specified fields from the Coveo organization. These fields are used throughout the UI layout, e.g., 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 top right corner, you can see the search bar. Try to type in "dress" or "shirt" and then press Enter to see the search results powered by Coveo.

Get the Advanced Demo

To see a more advanced implementation, feel free to examine the Coveo Demandware demo.

What's next for me?