Sitecore Search : How to Create an Effective Search Page

Divesh SharmaDivesh Sharma
7 min read

A modern search experience begins with the search box. A well-designed search input field with real-time suggestions can greatly enhance usability, lower bounce rates, and help users find what they're looking for more quickly.
In this post, we'll explore how to build an effective search page using Sitecore Search, including search suggestions as users type. This is the second part of our Sitecore Search series. In the previous post we have done configuration setup on CEC portal. In the next post, we'll look into how to create the results page that displays the search outcomes.

  1. Use REST APIs: This option is suitable when we are not using React and instead have a .NET-based Sitecore rendering SDK. The Customer Engagement Console (CEC) includes the API Explorer in the “Developer Resources” section. With this API Explorer, we can build a JSON request and retrieve data from the CEC using the endpoint URLs. The endpoint URL looks like this: https://discover.sitecorecloud.io/discover/v2/{domainID}

    You can also create a JSON request without using the API Explorer.

  2. Use Sitecore Search React SDK: This option is for React-based projects.

    We will use this option in our blog to create the component because it is very flexible and covers many use cases.

Query hooks

UI components interact with the Sitecore Search API using query hooks available in the JS SDK package. To populate and display merchandising widgets, you need to use React hooks to connect the components to the ReactQuery singleton in WidgetProvider. There are mainly four query hooks: usePreviewSearch, useRecommendation, useSearchResults, and useContentBlock.

For our component, we will use usePreviewSearch as it meets our needs.

For more info on query hooks you can refer to Sitecore docs.

Let’s start with installing the required packages -

@sitecore-search/react

@sitecore-search/ui

I hope you have set up Sitecore Search on the Customer Engagement Portal (CEC). If not, please check out my previous article, where we also created the widgets that we will use here.

After this let’s connect your Sitecore Search with your UI:

Open your \_app.tsx file and add the React component to it:

import { WidgetsProvider } from '@sitecore-search/react';

<WidgetsProvider {...SEARCH_CONFIG}>
       <Component {...rest} />
</WidgetsProvider>

Make sure to define the SEARCH_CONFIG above it which will contain all your keys.

  const SEARCH_CONFIG = {
    env: process?.env?.NEXT_PUBLIC_SEARCH_APP_ENV as Environment,
    customerKey: process?.env?.NEXT_PUBLIC_SEARCH_APP_CUSTOMER_KEY,
    apiKey: process?.env?.NEXT_PUBLIC_SEARCH_APP_API_KEY,
    useToken: true,
  };

You can store all your keys in your Netlify app or any other platform where your site is hosted and where your environment variables are set. For localhost, place the values in your .env file.

You will find these values in the "Developer Resources" section of your Customer Engagement portal (CEC). You can name the "env" as "prod" since it is the environment from which the data should be collected. Typical values include staging, uat, and prod.

Now that the connection is complete, let's create our SearchHero component.

Create a tsx file under your components folder: \src\components\SearchHero\SearchHero.tsx

import React from 'react';
import SearchHeroWidget from 'src/core/molecules/SearchHero/SearchHero';

export const Default = (): JSX.Element => {
  const rfkid = process?.env?.NEXT_PUBLIC_SEARCH_RFKID ?? 'results_1';
  return (
    <div className="">
      <SearchHeroWidget rfkId={rfkid} />
    </div>
  );
};

The rfkid is the ID of our preview search widget, which we created in our previous article. We can also configure this in our environment variable.

Now let’s create our component!

Create a tsx file in molecules folder: \src\core\molecules\SearchHero\SearchHero.tsx

Firstly, we create a model and mention all the attributes which we defined in our CEC and will be used here in this component.

type ArticleModel = {
  id: string;
  name: string;
  source_id?: string;
  url?: string;
};
type InitialState = PreviewSearchInitialState<'itemsPerPage'>;

Next part of code will be defining the component:

export const SearchHero= ({ defaultItemsPerPage = 6 }) => {
  const {
    widgetRef,
    actions: { onItemClick, onKeyphraseChange: onKeywordChange },
    queryResult,
    queryResult: { isFetching, isLoading },
  } = usePreviewSearch<ArticleModel, InitialState>({
    state: {
      itemsPerPage: defaultItemsPerPage,
    },
  });
  const loading = isLoading || isFetching;

This section includes the actions we need, such as:

  • onItemClick: This is triggered when we click an item in the search suggestions.

  • onKeyphraseChange: This is called when we enter a keyword in the search box.

There are additional actions available for PreviewSearch that can be used if needed: onSuggestionAdded, onSuggestionRemoved, onSuggestionClick, which are for the suggestions block.

We also have isFetching and isLoading, which are used for the loader that appears until we receive results from the API.

We can add a useEffect for the onKeywordChange functionality and a handleKeyDown to navigate to the search results page when the Enter key is pressed, updating the browser URL with the entered keyword.

useEffect(() => {
    if (keyword.length > 0) {
      onKeywordChange({ keyphrase: keyword || '' });
    }
  }, [keyword]);

  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
    if (event.key === 'Enter' || event.key === 'Next') {
      const url = `/results-page?keyword=${keyword}`;
      window.location.href = url;
    }
  };

Now let’s add some HTML in our file!

...
// add div and classes before for title-backgroundImage-padding etc.
...
<PreviewSearch.Root>
  <PreviewSearch.Input
    onChange={(e) => {
      const value = e.target.value;
      setKeyword(value);
      if (value.trim() === '') {
        onKeywordChange({ keyphrase: '' }); // Reset the query
      } else {
        onKeywordChange({ keyphrase: value });
      }
    }}
    onKeyDown={handleKeyDown}
    onFocus={() => (e: { target: { value: React.SetStateAction<string> } }) =>
      setKeyword(e.target.value)}
    value={keyword}
    autoComplete="off"
    placeholder="Enter keyword..."
  />
  <PreviewSearch.Content ref={widgetRef}>
    <Presence present={loading}>
      <div>
        <svg
          aria-busy={loading}
          aria-hidden={!loading}
          focusable="false"
          role="progressbar"
          viewBox="0 0 20 20"
        >
          <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
        </svg>
      </div>
    </Presence>
    <Presence present={!loading}>
      <PreviewSearch.Results defaultQueryResult={queryResult}>
        {({ isFetching: loading, data: { content: articles = [] } = {} }) => (
          <PreviewSearch.Items
            data-loading={loading}
            className={`w-full bg-white focus:outline-0 text-grey font-normal text-xl leading-[1.2] !px-3.5 ${
              keyword.length < 3 && !visibility ? 'hidden' : ''
            }`}
          >
            <Presence present={loading}>
              <div>
                <svg
                  aria-busy={loading}
                  aria-hidden={!loading}
                  focusable="false"
                  role="progressbar"
                  viewBox="0 0 20 20"
                >
                  <path d="M7.229 1.173a9.25 9.25 0 1 0 11.655 11.412 1.25 1.25 0 1 0-2.4-.698 6.75 6.75 0 1 1-8.506-8.329 1.25 1.25 0 1 0-.75-2.385z" />
                </svg>
              </div>
            </Presence>
            {!loading &&
              articles.map((article, index) => (
                <PreviewSearch.Item key={article.id} asChild>
                  <a
                    href={article.url}
                    onClick={(e) => {
                      e.preventDefault();
                      onItemClick({
                        id: article.id,
                        index,
                        sourceId: article.source_id,
                      });
                      // add redirection or any action
                    }}
                  >
                    <ArticleCard.Root>
                      <ArticleCard.Title>{article.name}</ArticleCard.Title>
                    </ArticleCard.Root>
                  </a>
                </PreviewSearch.Item>
              ))}
              {articles.length > 0 && (
              <a
                href={`/listing-page?keyword=${keyword}`}
              >
                <ArticleCard.Root className="py-2 !text-lg text-black hover:!text-red">
                  <ArticleCard.Content>View all results</ArticleCard.Content>
                </ArticleCard.Root>
              </a>
            )}
          </PreviewSearch.Items>
        )}
      </PreviewSearch.Results>
    </Presence>
  </PreviewSearch.Content>
</PreviewSearch.Root>
<div className={`flex flex-col items-start w-[100%] `}>
    <a
      className="w-full"
      href={`/results-page?keyword=${keyword}`}
    >
      <Button
        bgColor="white"
        className=" text-center w-1/2"
      >
        Search
      </Button>
    </a>
  </div>

...
// rest of your html with css

In this HTML, we created a banner with a box and a search button. When a user types in the box, suggestions appear below after entering 3 letters. We used the hidden CSS class for this. We mapped the articles based on the entered keyword, and they are displayed below. There is also a final item labeled “View all results,” which works the same as the search button. Each suggested item has a URL and a name. When the search button is clicked or the enter key is pressed, the next page is navigated to, and the URL includes the “keyword” in the query string like this: “https://abcd.com/results-page?keyword=${keyword}”.

You can add any more HTML and CSS as needed for your design.

You will also need to import, declare constant variables, and set or get the variables. Also, do not forget to add locale if you have multiple languages:

import { PageController } from '@sitecore-search/react';
import type { PreviewSearchInitialState } from '@sitecore-search/react';
import { WidgetDataType, usePreviewSearch, widget } from '@sitecore-search/react';
import { ArticleCard, Presence, PreviewSearch } from '@sitecore-search/ui';

const context = PageController.getContext();
context.setLocaleLanguage('en');
context.setLocaleCountry('us');
const [keyword, setKeyword] = React.useState('');

At the last after the return, we have to export the widget which can be done like:

const entity = process?.env?.NEXT_PUBLIC_SEARCH_APP_ENTITY_NAME ?? 'sitecoremerchandise';
const SearchHeroWidget = widget(SearchHero, WidgetDataType.PREVIEW_SEARCH, entity);
export default SearchHeroWidget;

If there are multiple entities or you have custom entity created, you can pass the value here. This widget exported was imported in our file \src\components\SearchHero\SearchHero.tsx

Our banner will look something like this after you add required html and css:

The suggested results will be generated by Sitecore Search based on textual relevance. Since we have added textual relevance for Name and Description, it will check if the entered word matches or partially matches (depending on the analyzer you use) the name or description data attribute from our website's content. We can also add suggestion blocks, synonyms, and more to enhance our search experience. Personalization, search ranking, and boost/bury rules are also available to help improve our results.

In this article, we built on the foundation laid in my previous post on setting up Sitecore Search on CEC, moving from initial configuration to designing an effective and user-focused search page. As we've seen, a great search experience is more than just a list of results—it’s about relevance, usability, and performance. With Sitecore Search's powerful AI capabilities, you can deliver content that truly connects with your audience.

Up next, we’ll take it a step further by creating a dynamic listing page with filters, sorting, and pagination—key features that enhance content discoverability and user control.

Stay tuned as we continue to shape an intelligent, end-to-end search and content discovery experience with Sitecore!

Reference used: Sitecore Search Documentation

0
Subscribe to my newsletter

Read articles from Divesh Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Divesh Sharma
Divesh Sharma

I am currently working in Genpact as a Sitecore Developer with 6+ years of experience. I am Sitecore 9 & 10 certified and my skills include Sitecore, Dotnet, NextJS, XM Cloud, Html, Css, Headless, Solr. I have completed my Bachelor of Engineering in Information Technology.