1. Developer guides

Create a search results page

Version:

You can use the Cloud SDK search package to build search experiences and capture events that occur inside them.

In this walkthrough, you'll create a simple search results page with a search field in a Next.js app. Specifically, you'll request search content from Sitecore Search, display it in the user interface, and capture events when a site visitor views the search results.

The walkthrough describes the typical workflow for creating search experiences. After completing it, you can tailor the requests and the presentation of the content to your own needs for this search results page, or build other types of search experiences.

This walkthrough describes how to:

Before you begin
  • Have a search results widget and a search results page published in Sitecore Search.

  • In your code editor, open the root folder of your Next.js app. This walkthrough was tested on Next.js version 15, both for the Pages Router and the App Router.

  • Install and initialize the Cloud SDK and the events and search packages.

  • Optionally, install Tailwind CSS in your app. This walkthrough uses Tailwind CSS to stylize the user interface, but it is not required to complete the walkthrough.

Request search content

The first step to creating a search results page is to request search content from Sitecore Search. In Sitecore Search, widgets hold this content, so you will request data for a specific widget.

  1. In your code editor, open the root folder of your Next.js app.

  2. Depending on your router type:

    • If using the Pages Router - in the pages folder, create a file called search.tsx.

    • If using the App Router - in the app folder, create a subfolder called search. Then, in the search folder, create a file called page.tsx.

  3. To import everything from the search package that's required to request data from Sitecore Search:

    • If using the Pages Router - paste the following code into search.tsx:

      import { useState, useEffect } from "react";
      import {
        Context,
        getWidgetData,
        SearchWidgetItem,
        WidgetRequestData
      } from "@sitecore-cloudsdk/search/browser";
      
      export default function SearchResultsPage(){
        return (<></>)
      }
    • If using the App Router - paste the following code into page.tsx:

      "use client";
      import { useState, useEffect } from "react";
      import {
        Context,
        getWidgetData,
        SearchWidgetItem,
        WidgetRequestData
      } from "@sitecore-cloudsdk/search/browser";
      
      export default function SearchResultsPage(){
        return (<></>)
      }
  4. Inside the default function, directly before the return statement, paste the following code then save your changes:

    // Create a data state variable to store the received data:
    const [products, setProducts] = useState<any[]>([]); // In production, replace `any[]` with the interface of your choice for your products
    
    // Perform the initial data request:
    useEffect(() => {
      async function fetchData() {
        const widgetRequest = new SearchWidgetItem("product", "rfkid_7"); // Create a new widget request
        widgetRequest.content = {}; // Request all attributes for the entity
        widgetRequest.limit = 10; // Limit the number of results to 10
        // widgetRequest.sources = ["12345"]; // Optionally, return results only from specific sources
    
        // Create a new context with the locale set to "EN" and "us".
        // Depending on your Sitecore Search configuration, using `Context` might be optional:
        const context = new Context({
          locale: { language: "EN", country: "us" },
        });
    
        // Call the `getWidgetData` function with the widget request and the context to request the data:
        const response = await getWidgetData(
          new WidgetRequestData([widgetRequest]),
          context
        );
    
        if (!response) return console.warn("No search results found.");
    
        // Set the received data to the state variable:
        const currentProducts = response.widgets?.[0]?.content || []; 
        setProducts(currentProducts);
      }
    
      fetchData();
    }, []);

    This code requests product data from Sitecore Search. Specifically, the code:

    • Creates an object called widgetRequest. This object describes a search results widget with the widget ID of "rfkid_7" and is configured to request index documents that belong to the Product entity in Sitecore Search. This widget is already set up in your instance of Sitecore Search to return search results.

    • Runs the getWidgetData function to request data associated with widgetRequest, and saves the data that Sitecore Search returns into the response variable.

    • Stores the returned data, if any, in the products state variable.

  5. Update the return statement to return the following, then save your changes:

    <>
      {products && (
        <ul>
          {products.map((product: any) => (
            <li key={product.id}>{product.name}</li>
          ))}
        </ul>
      )}
    </>

    This code iterates through the contents of the products state variable and displays each product index document in the user interface.

  6. In your terminal, enter npm run dev to start your Next.js app, then open the page you created in your web browser. A list of products displays. This is the content you requested from Sitecore Search.

  7. Open your web browser's developer tools and on the Network tab, start recording network activity.

    You'll use the Network tab while building search experiences to check the network requests you make to Sitecore.

  8. In your web browser, reload the page and then find a network request made to edge-platform.sitecorecloud.io/v1/search. This is the request you made for product index documents.

Capture events

After requesting search content and displaying it in the user interface, you update your code to capture events when your site visitor views the content.

The captured event data is sent to Sitecore and becomes available in Sitecore Search. This data lets you personalize search results and track the performance of your search experiences so you can refine and improve them.

To capture events:

  1. In the file you worked with in the previous procedure, at the top of the file, add widgetView to the list of imports from the search package. You'll use this function in the next step to start tracking when the widget is viewed in your app.

  2. In the default function, in the Effect Hook, directly after setProducts(currentProducts);, paste the following code:

    widgetView({
      request: {},
      entities: currentProducts.map((product: any) => ({
        entity: "product",
        id: product.id,
      })),
      pathname: "/search",
      widgetId: "rfkid_7",
    });

    This code sends a widget view event every time the search results are viewed.

  3. In your web browser, reload the page. When the webpage with the search experience loads, the widget view event triggers and is sent to Sitecore.

    Tip

    Use your web browser's developer tools to check the payload that is sent to Sitecore.

  4. Optionally, to find the event in Sitecore Search:

    1. In your web browser's developer tools, view the cookies for your app.

    2. Find a cookie named sc_{SitecoreEdgeContextId} , where {SitecoreEdgeContextId} is your Context ID.

    3. Copy the cookie value. This value is the browser ID. You'll use it to find the event in Sitecore Search.

      Example: a38b230c-11eb-4cf9-8d5d-274e9f344925​

    4. In Sitecore Search > Developer Resources > Event Monitor, paste the browser ID in the UUID field, then click Start Monitoring.

    5. In your web browser, reload the search results page. When the webpage loads, the widget view event triggers, is sent to Sitecore Search, and the Event Monitor displays it.

    6. In Sitecore Search, check that the Event Monitor now lists events. These are the events you sent when you reloaded the search results page in your app.

Stylize the user interface

After capturing events, you continue updating your code to stylize the user interface in your app.

To stylize the user interface:

  1. In the file you worked with in the previous procedure, directly after the list of imports and before the default function, paste the following code:

    const ProductList = ({ products }: { products: any }) => {
      return (
        <div className="flex-1 relative">
          <div className="grid grid-cols-1 gap-6">
            {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
    
            {products?.map((item: any, index: number) => (
              <div
                key={item.id}
                className="product-item bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200"
              >
                <div className="flex p-6">
                  <div className="flex-shrink-0 mr-6">
                    <img
                      src={item.image_url}
                      alt={item.name}
                      width={120}
                      height={120}
                      className="rounded-lg object-cover"
                    />
                  </div>
    
                  <div className="flex-1">
                    <div className="flex justify-between items-start">
                      <div>
                        <span className="text-sm text-gray-500">{item.brand}</span>
    
                        <h2 className="text-xl font-semibold text-gray-900 mt-1">
                          {item.name}
                        </h2>
    
                        <span>{item.id}</span>
    
                        <p className="text-lg font-medium text-red-600 mt-2">
                          € {item.price}
                        </p>
                      </div>
                    </div>
    
                    {item.description && (
                      <p className="text-gray-600 mt-3 text-sm line-clamp-2">
                        {item.description}
                      </p>
                    )}
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      );
    };

    This code creates a ProductList component.

  2. Update the return statement to return the following, then save your changes:

    <div className="container mx-auto px-4 py-8">
      <div className="flex justify-between items-center mb-6">
        <h2 className="text-2xl font-bold text-gray-900">Search Results</h2>
      </div>
    
      <ProductList products={products} />
    </div>

    This code renders the ProductList component, including all the product index documents that you request from Sitecore Search.

  3. In your web browser, reload the search results page. The webpage now loads with Tailwind CSS styles applied.

Add a search field

You have now successfully requested search content and displayed it in the user interface. Next, you add a search input field to the webpage to display search results in response to what the site visitor searches for.

To add a search field:

  1. Depending on your router type:

    • If using the Pages Router - replace the contents of search.tsx with the following, then save your changes:

      import { useState, useEffect } from "react";
      import {
        Context,
        getWidgetData,
        SearchWidgetItem,
        WidgetRequestData,
        widgetView
      } from "@sitecore-cloudsdk/search/browser";
      
      const fetchSearchRequest = async (searchTerm: string) => {
        const widgetRequest = new SearchWidgetItem("product", "rfkid_7"); // Create a new widget request
        widgetRequest.content = {}; // Request all attributes for the entity
        widgetRequest.limit = 10; // Limit the number of results to 10
        // widgetRequest.sources = ["12345"]; // Optionally, return results only from specific sources
      
        // Use the search term, if any, to request search content:
        if (searchTerm) {
          widgetRequest.query = {
            keyphrase: searchTerm,
          };
        }
      
        // Create a new context with the locale set to "EN" and "us".
        // Depending on your Sitecore Search configuration, using `Context` might be optional:
        const context = new Context({
          locale: { language: "EN", country: "us" },
        });
      
        // Call the `getWidgetData` function with the widget request and the context to request the data:
        const response = await getWidgetData(
          new WidgetRequestData([widgetRequest]),
          context
        );
      
        if (!response) {
          console.warn("No search results found.");
          return [];
        }
      
        // Set the received data to the state variable:
        const currentProducts = response.widgets?.[0]?.content || [];
        return currentProducts;
      };
      
      const ProductList = ({ products }: { products: any }) => {
        return (
          <div className="flex-1 relative">
            <div className="grid grid-cols-1 gap-6">
              {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
      
              {products?.map((item: any, index: number) => (
                <div
                  key={item.id}
                  className="product-item bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200"
                >
                  <div className="flex p-6">
                    <div className="flex-shrink-0 mr-6">
                      <img
                        src={item.image_url}
                        alt={item.name}
                        width={120}
                        height={120}
                        className="rounded-lg object-cover"
                      />
                    </div>
      
                    <div className="flex-1">
                      <div className="flex justify-between items-start">
                        <div>
                          <span className="text-sm text-gray-500">{item.brand}</span>
      
                          <h2 className="text-xl font-semibold text-gray-900 mt-1">
                            {item.name}
                          </h2>
      
                          <span>{item.id}</span>
      
                          <p className="text-lg font-medium text-red-600 mt-2">
                            € {item.price}
                          </p>
                        </div>
                      </div>
      
                      {item.description && (
                        <p className="text-gray-600 mt-3 text-sm line-clamp-2">
                          {item.description}
                        </p>
                      )}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        );
      };
      
      export default function SearchResultsPage() {
        // Create a data state variable to store the received data:
        const [products, setProducts] = useState<any[]>([]); // In production, replace `any[]` with the interface of your choice for your products
      
        // Create a state variable for the search term:
        const [searchTerm, setSearchTerm] = useState("");
        const searchTermHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
          setSearchTerm(event.target.value);
        };
      
        // Perform the initial data request:
        useEffect(() => {
          async function fetchData() {
            const currentProducts = await fetchSearchRequest(searchTerm);
            setProducts(currentProducts);
      
            widgetView({
              request: {},
              entities: currentProducts.map((product: any) => ({
                entity: "product",
                id: product.id,
              })),
              pathname: "/search",
              widgetId: "rfkid_7",
            });
          }
      
          fetchData();
        }, []);
      
        // Request data based on the search term:
        useEffect(() => {
          async function fetchData() {
            const currentProducts = await fetchSearchRequest(searchTerm);
            setProducts(currentProducts);
          }
      
          fetchData();
        }, [searchTerm]);
      
        return (
          <div className="container mx-auto px-4 py-8">
            <input
              className="w-full p-2 border border-gray-300 rounded-md mb-2"
              type="text"
              placeholder="Search products"
              onChange={searchTermHandler}
            />
      
            <div className="flex justify-between items-center mb-6">
              <h2 className="text-2xl font-bold text-gray-900">
                Search Results {searchTerm ? ` for ${searchTerm}` : ""}
              </h2>
            </div>
      
            <ProductList products={products} />
          </div>
        )
      }
    • If using the App Router - replace the contents of page.tsx with the following, then save your changes:

      "use client";
      import { useState, useEffect } from "react";
      import {
        Context,
        getWidgetData,
        SearchWidgetItem,
        WidgetRequestData,
        widgetView
      } from "@sitecore-cloudsdk/search/browser";
      
      const fetchSearchRequest = async (searchTerm: string) => {
        const widgetRequest = new SearchWidgetItem("product", "rfkid_7"); // Create a new widget request
        widgetRequest.content = {}; // Request all attributes for the entity
        widgetRequest.limit = 10; // Limit the number of results to 10
        // widgetRequest.sources = ["12345"]; // Optionally, return results only from specific sources
      
        // Use the search term, if any, to request search content:
        if (searchTerm) {
          widgetRequest.query = {
            keyphrase: searchTerm,
          };
        }
      
        // Create a new context with the locale set to "EN" and "us".
        // Depending on your Sitecore Search configuration, using `Context` might be optional:
        const context = new Context({
          locale: { language: "EN", country: "us" },
        });
      
        // Call the `getWidgetData` function with the widget request and the context to request the data:
        const response = await getWidgetData(
          new WidgetRequestData([widgetRequest]),
          context
        );
      
        if (!response) {
          console.warn("No search results found.");
          return [];
        }
      
        // Set the received data to the state variable:
        const currentProducts = response.widgets?.[0]?.content || [];
        return currentProducts;
      };
      
      const ProductList = ({ products }: { products: any }) => {
        return (
          <div className="flex-1 relative">
            <div className="grid grid-cols-1 gap-6">
              {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
      
              {products?.map((item: any, index: number) => (
                <div
                  key={item.id}
                  className="product-item bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200"
                >
                  <div className="flex p-6">
                    <div className="flex-shrink-0 mr-6">
                      <img
                        src={item.image_url}
                        alt={item.name}
                        width={120}
                        height={120}
                        className="rounded-lg object-cover"
                      />
                    </div>
      
                    <div className="flex-1">
                      <div className="flex justify-between items-start">
                        <div>
                          <span className="text-sm text-gray-500">{item.brand}</span>
      
                          <h2 className="text-xl font-semibold text-gray-900 mt-1">
                            {item.name}
                          </h2>
      
                          <span>{item.id}</span>
      
                          <p className="text-lg font-medium text-red-600 mt-2">
                            € {item.price}
                          </p>
                        </div>
                      </div>
      
                      {item.description && (
                        <p className="text-gray-600 mt-3 text-sm line-clamp-2">
                          {item.description}
                        </p>
                      )}
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        );
      };
      
      export default function SearchResultsPage() {
        // Create a data state variable to store the received data:
        const [products, setProducts] = useState<any[]>([]); // In production, replace `any[]` with the interface of your choice for your products
      
        // Create a state variable for the search term:
        const [searchTerm, setSearchTerm] = useState("");
        const searchTermHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
          setSearchTerm(event.target.value);
        };
      
        // Perform the initial data request:
        useEffect(() => {
          async function fetchData() {
            const currentProducts = await fetchSearchRequest(searchTerm);
            setProducts(currentProducts);
      
            widgetView({
              request: {},
              entities: currentProducts.map((product: any) => ({
                entity: "product",
                id: product.id,
              })),
              pathname: "/search",
              widgetId: "rfkid_7",
            });
          }
      
          fetchData();
        }, []);
      
        // Request data based on the search term:
        useEffect(() => {
          async function fetchData() {
            const currentProducts = await fetchSearchRequest(searchTerm);
            setProducts(currentProducts);
          }
      
          fetchData();
        }, [searchTerm]);
      
        return (
          <div className="container mx-auto px-4 py-8">
            <input
              className="w-full p-2 border border-gray-300 rounded-md mb-2"
              type="text"
              placeholder="Search products"
              onChange={searchTermHandler}
            />
      
            <div className="flex justify-between items-center mb-6">
              <h2 className="text-2xl font-bold text-gray-900">
                Search Results {searchTerm ? ` for ${searchTerm}` : ""}
              </h2>
            </div>
      
            <ProductList products={products} />
          </div>
        )
      }

    Note the following differences in the updated code compared to the previous one:

    • The search term, if any, is stored in the searchTerm state variable.

    • The search request logic now uses the search term, if the site visitor enters any, to request search content that matches that specific search term.

    • The search request logic is now separated into a new function called fetchSearchRequest. This ensures that the logic can be run in multiple places in the code.

    • A second Effect Hook requests new data when the search term changes.

    • In the return statement, an input field is now also rendered.

  2. In your web browser, reload the search results page. A search field now also displays on the page.

  3. Enter a search term in the search field and note that the search results change.

  4. In your web browser's developer tools, on the Network tab, check how the network requests change when you enter a different search term in the search field.

Next steps

You've now successfully created a search results page with a search field and started capturing events when the site visitor views the results.

Next, you can:

If you have suggestions for improving this article, let us know!