1. Developer guides

Create a questions-answers widget

Version:

You can use the Cloud SDK search package to build search experiences, such as a questions-answers (Q&A) widget. This widget lets you show site visitors an AI-generated answer to their question, and optionally, more questions related to their query.

In this walkthrough, you'll create a simple Q&A widget in a Next.js app. Specifically, you'll request Q&A content from Sitecore Search and display it in the user interface.

The walkthrough describes the typical workflow for creating a questions-answers widget. After completing it, you can tailor the requests and the presentation of the content to your own needs for this Q&A widget, or build other types of search experiences.

This walkthrough describes how to:

Before you begin
  • Make sure the Questions & Answers capability is configured in Sitecore Search and that batch generation runs at least once. Batch generation automatically creates the "rfkid_qa" widget, which is required for requesting questions and answers.

  • 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 and display questions and answers

The first step to creating a Q&A widget is to request 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 questions.tsx.

    • If using the App Router - in the app folder, create a subfolder called questions. Then, in the questions 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 questions.tsx:

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

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

    // Create state variables to store the received data and loading state:
    const [isLoading, setIsLoading] = useState(true);
    const [mainQuestion, setMainQuestion] = useState<any>({}); // In production, replace `any` with a specific interface
    const [relatedQuestions, setRelatedQuestions] = useState<any[]>([]); // In production, replace `any[]` with a specific interface
    
    // Perform the initial data request:
    useEffect(() => {
      async function fetchData() {
        const widgetRequest = new QuestionsAnswersWidgetItem(
          "content",
          "rfkid_qa",
          {
            keyphrase: "What is sitecoreai",
            exactAnswer: {},
            relatedQuestions: {
              limit: 5,
            },
          }
        );
    
        // 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 variables:
        setMainQuestion(response.widgets?.[0].answer || {});
        setRelatedQuestions(response.widgets?.[0].related_questions || []);
        setIsLoading(false);
      }
      fetchData();
    }, []);

    This code requests Q&A data from Sitecore Search. Specifically, the code:

    • Creates an object called widgetRequest using the QuestionsAnswersWidgetItem class. This object describes a questions-answers widget with the widget ID of "rfkid_qa" and is configured to find an answer for the keyphrase "What is sitecoreai". It also retrieves maximum five other questions related to this query.

      XM Cloud is now SitecoreAI

      Some code examples, images, and UI labels may still use XM Cloud while engineering assets are being updated.

      Note

      Always use "rfkid_qa" to request a questions-answers widget.

    • 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 main question and related questions, if any, in the mainQuestion and relatedQuestions state variables.

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

    if (isLoading) {
      return <div>Loading...</div>;
    }
    
    return (
      <div>
        <h2>Question</h2>
        <p>{mainQuestion?.question}</p>
        <h2>Answer</h2>
        <p>{mainQuestion?.answer}</p>
        <h3>Related Questions</h3>
        <ul>
          {relatedQuestions.map((question: any) => (
            <li key={question.id}>
              <h4>{question.question}</h4>
              <p>{question.answer}</p>
            </li>
          ))}
        </ul>
      </div>
    );

    This code displays a loading message while retrieving data. After the data is retrieved, it iterates through the contents of the state variables and displays the main question and answer, followed by a list of related questions.

  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 main question and a list of related questions display. 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 the Q&A content.

Stylize the user interface

After requesting and displaying the content, you continue updating your code to stylize the user interface in your app.

To stylize the user interface:

XM Cloud is now SitecoreAI

Some code examples, images, and UI labels may still use XM Cloud while engineering assets are being updated.

  1. Depending on your router type:

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

      import { useEffect, useState } from "react";
      import {
        Context,
        getWidgetData,
        QuestionsAnswersWidgetItem,
        WidgetRequestData,
      } from "@sitecore-cloudsdk/search/browser";
      
      export default function QuestionsPage() {
        const [isLoading, setIsLoading] = useState(true);
        const [mainQuestion, setMainQuestion] = useState<any>({}); // In production, replace `any` with a specific interface
        const [relatedQuestions, setRelatedQuestions] = useState<any[]>([]); // In production, replace `any[]` with a specific interface
      
        useEffect(() => {
          async function fetchData() {
            const widgetRequest = new QuestionsAnswersWidgetItem(
              "content",
              "rfkid_qa",
              {
                keyphrase: "What is xm cloud",
                exactAnswer: {},
                relatedQuestions: {
                  limit: 5,
                },
              }
            );
      
            // widgetRequest.sources = ["12345"]; // Optionally, return results only from specific sources
            
            const context = new Context({
              locale: { language: "EN", country: "us" },
            });
      
            const response = await getWidgetData(
              new WidgetRequestData([widgetRequest]),
              context
            );
      
            if (!response) return console.warn("No search results found");
            setMainQuestion(response.widgets?.[0].answer || {});
            setRelatedQuestions(response.widgets?.[0].related_questions || []);
            setIsLoading(false);
          }
          fetchData();
        }, []);
      
        if (isLoading) {
          return <div>Loading ...</div>;
        }
      
        return (
          <div className="bg-[image:var(--sc-bg)] p-4 flex items-center justify-center flex-col">
            <div className="container mx-auto">
              <div className="rounded-md border border-gray-200 dark:border-gray-600 p-3 my-5 shadow-md shadow-slate-300 dark:shadow-slate-500">
                <div className="p-3 dark:text-gray-100">
                  <h2 className="text-lg font-bold ">{mainQuestion?.question}</h2>
                  <p className="mt-2 text-sm">{mainQuestion?.answer}</p>
                </div>
                <div className="w-full mt-4 px-2">
                  <h3 className="text-md font-bold mb-4">People also ask ...</h3>
                  {relatedQuestions.map((question: any) => (
                    <div key={question.id} className="w-full cursor-pointer border-b dark:border-b-gray-600 py-4">
                      <div className="w-full flex justify-between gap-x-2 text-left text-sm">
                        <span>{question.question}</span>
                      </div>
                      <div className="pt-5 font-light text-sm">{question.answer}</div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        );
      };
    • If using the App Router - replace the contents of page.tsx with the following, then save your changes:

      "use client";
      import { useEffect, useState } from "react";
      import {
        Context,
        getWidgetData,
        QuestionsAnswersWidgetItem,
        WidgetRequestData,
      } from "@sitecore-cloudsdk/search/browser";
      
      export default function QuestionsPage() {
        const [isLoading, setIsLoading] = useState(true);
        const [mainQuestion, setMainQuestion] = useState<any>({}); // In production, replace `any` with a specific interface
        const [relatedQuestions, setRelatedQuestions] = useState<any[]>([]); // In production, replace `any[]` with a specific interface
      
        useEffect(() => {
          async function fetchData() {
            const widgetRequest = new QuestionsAnswersWidgetItem(
              "content",
              "rfkid_qa",
              {
                keyphrase: "What is xm cloud",
                exactAnswer: {},
                relatedQuestions: {
                  limit: 5,
                },
              }
            );
      
            // widgetRequest.sources = ["12345"]; // Optionally, return results only from specific sources
            
            const context = new Context({
              locale: { language: "EN", country: "us" },
            });
      
            const response = await getWidgetData(
              new WidgetRequestData([widgetRequest]),
              context
            );
      
            if (!response) return console.warn("No search results found");
            setMainQuestion(response.widgets?.[0].answer || {});
            setRelatedQuestions(response.widgets?.[0].related_questions || []);
            setIsLoading(false);
          }
          fetchData();
        }, []);
      
        if (isLoading) {
          return <div>Loading ...</div>;
        }
      
        return (
          <div className="bg-[image:var(--sc-bg)] p-4 flex items-center justify-center flex-col">
            <div className="container mx-auto">
              <div className="rounded-md border border-gray-200 dark:border-gray-600 p-3 my-5 shadow-md shadow-slate-300 dark:shadow-slate-500">
                <div className="p-3 dark:text-gray-100">
                  <h2 className="text-lg font-bold ">{mainQuestion?.question}</h2>
                  <p className="mt-2 text-sm">{mainQuestion?.answer}</p>
                </div>
                <div className="w-full mt-4 px-2">
                  <h3 className="text-md font-bold mb-4">People also ask ...</h3>
                  {relatedQuestions.map((question: any) => (
                    <div key={question.id} className="w-full cursor-pointer border-b dark:border-b-gray-600 py-4">
                      <div className="w-full flex justify-between gap-x-2 text-left text-sm">
                        <span>{question.question}</span>
                      </div>
                      <div className="pt-5 font-light text-sm">{question.answer}</div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
          </div>
        );
      };

    This updated code replaces the basic HTML structure in the return statement with styled components using Tailwind CSS classes.

  2. In your web browser, reload the Q&A page. The webpage now loads with Tailwind CSS styles applied.

Next steps

You've now successfully created a questions-answers widget.

Next, you can:

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