Sitecore Search: How to Build Components
Today, I’d like to walk you through the process of creating a widget and implementing a search component using React for our widget. Let’s start by setting up the widget.
Sitecore Search offers several pre-built widgets, including Preview Search, Search Result, Recommendation, Banner, and HTML Block. Each widget comes with its own unique set of features as well as common capabilities. For instance, the Recommendation widget includes a “recipe” feature, which is exclusive to this widget and not available in others.
Within each widget, you have the flexibility to define rules, filters, and conditions to control how data is presented. For example, you can configure a widget to display only items from Italy that carry a specific tag. Furthermore, if you’re using Sitecore’s personalization features, you can apply filters based on personalization data, such as displaying content exclusively to users from Italy.
Sitecore Search’s powerful widgets are designed to allow configuration without the need for additional coding, making them highly customizable and efficient for various use cases.
In this article, I’ll guide you through creating a Search Result widget. To begin, click on the "Add Widget" button, then select the Search Result widget from the options.
When setting up the widget, you'll need to configure the following fields:
The most important field is the RFK ID, as this identifier will be used in your queries within Sitecore Search. I recommend naming this ID without spaces and ensuring it is easily understandable for future reference.
I created a widget with the ID content_search and selected the option to apply this widget across all pages. Once the widget is created, you’ll see two sections: Widget Variations and Widget Settings. By default, one variation will be created for your widget, and I’ll use the default variation.
Clicking on the variation will open a page where you can configure the widget. Here, you can define a variety of rules such as Slots, Boost Rules, Bury Rules, Blacklist Rules, Facets, Textual Relevance, Search Ranking, Personalization, and Sorting. I won’t cover all of these settings, as they are detailed extensively in the Sitecore Search Documentation.
In this article, my goal is to demonstrate how to create a simple widget and use it within a React application. I won’t be configuring any rules for the widget; instead, I’ll display all source items and filter them by keywords.
After creating the widget, we can test it using the Developer Resources section. To do this, select the API Explorer tool. The API Explorer is a very useful interface for testing your widget, offering a wide range of settings to create and execute query bodies.
For testing our widget, I created a simple query as follows:
{
"widget": {
"items": [
{
"rfk_id": "content_search",
"entity": "corporatepage",
"search": {
"content": {}
}
}
]
}
}
After clicking Execute, I received the first 10 results with the entity type set to corporatepage. The required fields for the query are rfk_id and entity. In the search section, we specify which fields we want to return; if this section is left empty, all available fields from the entity will be returned.
Note: Only fields that are marked as “Return in API” in the attribute settings will be included in the response.
领英推荐
As you can see, our widget is working and returning results. Additionally, we can enhance the search functionality by adding the parameter search.query.keyphrase, which allows us to search by keywords.
Now, let’s move on to implementing this in React. Sitecore Search provides a React SDK, which includes everything you need to build and integrate your search components. If you're using other JavaScript frameworks, you'll need to implement your own API methods to retrieve data from Sitecore Search. However, I believe the Sitecore team may add support for other frameworks in the future.
For implementing the React component, the following resources are helpful:
Of these, the second resource, Storybook, has been particularly useful to me. It provides numerous examples of how to implement various components, making it an excellent reference for building your own.
Now, let’s implement a search feature based on our previously created widget. The requirements for the search functionality are as follows:
For better control search confugurations, I recommend creating a Provider. Let's create a SitecoreSearchProvider to manage our configuration:
const SitecoreSearchProvider = ({ children }: Props): JSX.Element => {
const searchConfig = {
trackConsent: true,
publicSuffix: true,
debug: true,
env: '<environment>',
customerKey: '<environment>',
apiKey: '<API key provided in CEC>',
};
const { language, country } = useParsedLocale();
PageController.getContext().setLocaleLanguage(language);
PageController.getContext().setLocaleCountry(country);
return <WidgetsProvider {...searchConfig}>{children}</WidgetsProvider>;
};
export default SitecoreSearchProvider;
From the code, you can see that we set up keys for Sitecore Search and capture user information such as location and language. This information is crucial for enhancing the search experience, and it can be leveraged for future personalization efforts.
After that we can implement our Listing Search result component.
import { useState, FormEvent, useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import { usePathname, useSearchParams } from 'next/navigation';
import { ComponentProps } from 'lib/component-props';
import SearchProvider from 'src/components/ui/search/search-provider';
import SearchResult from './components/SearchResult';
import NextImage from 'next/image';
import { useI18n } from 'next-localization';
// import static images
import searchIcon from '@/images/icons/search.svg';
type ListingSearchResultsProps = ComponentProps & {
fields: {
pageSize?: string;
};
};
const ListingSearchResults = ({ fields }: ListingSearchResultsProps): JSX.Element => {
const SEARCH_TERM_NAME = 'searchInput';
const { pageSize = '5' } = fields;
const { t: translate } = useI18n();
const searchBlockRef = useRef<null | HTMLDivElement>(null);
const pathname = usePathname();
const searchParams = useSearchParams();
const query = searchParams.get(SEARCH_TERM_NAME);
const { push: replaceQuery, isReady: routerIsReady } = useRouter();
const [searchTerm, setSearchTerm] = useState<string | null>('');
const [phrase, setPhrase] = useState<string | null>('');
const rfkId = '<your widget rfkId>';
const parsedItemSize = parseInt(pageSize);
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
setPhrase(searchTerm);
replaceQuery(
{
pathname: pathname,
query: { [SEARCH_TERM_NAME]: searchTerm },
},
undefined,
{ shallow: true }
);
};
useEffect(() => {
if (routerIsReady) {
setSearchTerm(query);
setPhrase(query);
}
}, [routerIsReady, query]);
return (
<SearchProvider>
<div className="cc-em cc-search-results search-result-block" ref={searchBlockRef}>
<div className="container">
<div className="cc-search-results__form">
<form onSubmit={handleSubmit}>
<fieldset>
<legend className="cc-visibility-hidden">{searchText}</legend>
<div className="cc-search-form-group">
<div className="cc-search-content-input">
<label className="cc-label cc-visibility-hidden" htmlFor="searchInputText">
{searchTitle}
</label>
<input
className="cc-input"
name="searchInputText"
id="searchInputText"
type="text"
placeholder={searchText}
value={searchTerm || ''}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button
className="cc-button-input jq-search-listingSearchResults"
type="submit"
>
<NextImage className="cc-icon" src={searchIcon} alt={searchText} />
</button>
</div>
</div>
</fieldset>
</form>
</div>
<SearchResult
rfkId={rfkId}
itemsToDisplay={parsedItemSize}
phrase={phrase || ''}
searchBlockRef={searchBlockRef}
/>
</div>
</div>
</SearchProvider>
);
};
export default ListingSearchResults;
In the ListingSearchResults component, we configure the widget's RFK ID, define the page size, and implement a search field for keyword input. Additionally, you can see that I utilize a child component, SearchResult, within the main component. Now, let’s review how the SearchResult component functions and contributes to the overall search implementation.
import React, { useEffect, RefObject, useState } from 'react';
import { useI18n } from 'next-localization';
import { WidgetDataType, widget, useSearchResults } from '@sitecore-search/react';
import { Link } from '@sitecore-jss/sitecore-jss-nextjs';
import Pagination from 'src/components/ui/pagination';
type SearchResultProps = {
itemsToDisplay?: number;
phrase?: string;
searchBlockRef: RefObject<null | HTMLDivElement>;
};
type ListingProps = {
title: string;
description: string;
url: string;
update_date: number;
id: string;
};
export const SearchResult = ({
itemsToDisplay,
phrase = '',
searchBlockRef,
}: SearchResultProps) => {
const { t: translate, locale } = useI18n();
const langAndLocale = locale();
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const {
widgetRef,
state: { keyphrase, page, itemsPerPage = 1 },
actions: { onKeyphraseChange, onPageNumberChange },
queryResult: {
isLoading,
data: { content: listingItems = [], total_item: totalCount = 0 } = {},
},
} = useSearchResults({
query: (query) => {
query.getRequest();
},
state: {
itemsPerPage: itemsToDisplay,
keyphrase: phrase,
},
});
const totalPages = Math.ceil(totalCount / itemsPerPage);
useEffect(() => {
onKeyphraseChange({ keyphrase: phrase });
}, [onKeyphraseChange, phrase]);
useEffect(() => {
isFirstLoad && setIsFirstLoad(false);
searchBlockRef?.current &&
page &&
!isFirstLoad &&
searchBlockRef.current.scrollIntoView({
block: 'start',
behavior: 'smooth',
});
}, [page, searchBlockRef]);
return (
<div ref={widgetRef}>
<div className="cc-search-results-container">
{!isLoading && (
<p className="cc-search-results__text">{`${totalCount} ${'Not Found'}`}</p>
)}
<div className="cc-search-results__items">
{listingItems.map(
({ breadcrumbs = [], title, description, url, update_date, id }: ListingProps) => {
const date = new Date(update_date * 1000);
const computedDate = date.toLocaleDateString(langAndLocale, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
return (
<div className="cc-em cc-search-results-item" key={id}>
<span className="cc-press-releases-item__data">{computedDate}</span>
<Link field={{ href: url }}>
<h3 className="cc-press-releases-item__title">{title}</h3>
</Link>
<div className="cc-press-releases-item__text-wrp">
<p className="cc-press-releases-item__text cc-clamp cc-clamp--m-7 cc-clamp--d-3">
{description}
</p>
</div>
<div className="cc-simple-link">
<Link field={{ href: url }}>
{translate('Search/ListingSearchResults/Read all')}
</Link>
</div>
</div>
);
}
)}
</div>
</div>
<Pagination
onPageNumberChange={onPageNumberChange}
currentPage={page}
totalPages={totalPages}
/>
</div>
);
};
const RelatedResults = widget(SearchResult, WidgetDataType.SEARCH_RESULTS, 'corporatepage');
export default RelatedResults;
The SearchResult component is the core part of our search functionality. It handles the search query to Sitecore Search, retrieves the results, and displays them to the user. Additionally, it includes important business logic, such as checking the number of results and displaying a "Not Found" message when appropriate. It also prepares the results for pagination, ensuring a smooth user experience when navigating through multiple pages of results.
Additionally, the component includes logic for implementing pagination. This ensures that the results are displayed across multiple pages, allowing users to navigate through them easily. Pagination is a crucial part of the user experience, especially when dealing with large datasets, as it prevents overwhelming the user with too much information at once.
import NextImage from 'next/image';
import { useI18n } from 'next-localization';
import { Pagination } from '@sitecore-search/ui';
import { ActionProp, WidgetAction } from '@sitecore-search/react';
// import static images
import chevronLeft from '@/images/icons/chevron-left.svg';
import chevronRight from '@/images/icons/chevron-right.svg';
type PagerProps = {
currentPage: number;
defaultCurrentPage?: number;
totalPages: number;
onPageNumberChange: ActionProp<PagesProps>;
};
type PagesProps = WidgetAction & {
page: number;
};
const Pager = ({
currentPage = 1,
defaultCurrentPage = 1,
totalPages = 0,
onPageNumberChange,
}: PagerProps): JSX.Element => {
const { t: translate } = useI18n();
if (totalPages <= 1) return <></>;
const prevText = translate('Search/Paging/Previous page');
const nextText = translate('Search/Paging/Next page');
return (
<div className="cc-pagination">
<div className="container">
<Pagination.Root
currentPage={currentPage}
defaultCurrentPage={defaultCurrentPage}
totalPages={totalPages}
onPageChange={(v) => onPageNumberChange({ page: v })}
aria-label="Pagination Navigation"
>
<Pagination.Pages className="cc-pagination__items">
{(pagination) => {
const { currentPage } = pagination;
return (
<>
{currentPage !== 1 && (
<li className="cc-pagination__item">
<Pagination.PrevPage className="cc-pagination__link cc-pagination__link--arrow">
<NextImage className="cc-icon" src={chevronLeft} alt={prevText} />
</Pagination.PrevPage>
</li>
)}
{Pagination.paginationLayout(pagination, {
siblingCount: 1,
boundaryCount: 1,
}).map(({ page, type }, index) => (
<li className="cc-pagination__item" key={index}>
{type === 'page' ? (
<Pagination.Page
className={`cc-pagination__link cc-pagination__link--clickable ${
currentPage === page ? 'cc-pagination__link--active' : ''
}`}
aria-label={`Page ${page}`}
page={page || 1}
onClick={(e) => e.preventDefault()}
>
{page}
</Pagination.Page>
) : (
<span className="cc-pagination__link cc-pagination__link--dots" key={type}>
...
</span>
)}
</li>
))}
{currentPage !== totalPages && (
<li className="cc-pagination__item">
<Pagination.NextPage
onClick={(e) => e.preventDefault()}
className="cc-pagination__link cc-pagination__link--arrow"
>
<NextImage className="cc-icon" src={chevronRight} alt={nextText} />
</Pagination.NextPage>
</li>
)}
</>
);
}}
</Pagination.Pages>
</Pagination.Root>
</div>
</div>
);
};
export default Pager;
Conclusion
I hope this article helps you in creating your first component for Sitecore Search. I have demonstrated a real-world solution, but of course, you can adapt and simplify it to suit your needs. The React SDK makes implementing components straightforward, and the Sitecore team.