Tailwind Logo

Adding a Search Widget to a Next.js project (Part 1)

SearchNext.js

Published: 2023-10-05

Until now, the blog has used the Sitecore Search SDK to display search results. In this article, we will show you how it actually works by adding a widget to the plain Next.js environment.

Create a Next.js project

This time, we want to create a plain Next.js project, which we will call Pages Router, assuming that it can be used with Headless SXA provided by XM Cloud, and turn off Tailwind.css by default (we will turn it on later).

Plain Text
npx create-next-app
searchsample01.png

The sample code is now ready.

Adding packages to a project

If you refer to the Sitecore Search SDK documentation, you will find instructions for installing the package on the Getting Started page.

Install @sitecore-search/react and @sitecore-search/ui. The executed code is as follows.

PowerShell
npm install --save @sitecore-search/react
npm install --save @sitecore-search/ui
searchsample02.png

Next, add values to .env to connect to Sitecore Search. In this case, we added the following to .env

Plain Text
NEXT_PUBLIC_SEARCH_ENV=
NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=
NEXT_PUBLIC_SEARCH_API_KEY=
NEXT_PUBLIC_SEARCH_PATH=/

The above values can be found on the Devleoper Resource screen in Search. The values can be prod, staging, prodEu or apse2. For this, use the name given in the URL if it is not a subdomain.

To make this value easier to use, a file called constants/search.ts was created and the following code was written

TypeScript
export const SEARCH_ENV = process.env.NEXT_PUBLIC_SEARCH_ENV;
export const SEARCH_CUSTOMER_KEY = process.env.NEXT_PUBLIC_SEARCH_CUSTOMER_KEY;
export const SEARCH_API_KEY = process.env.NEXT_PUBLIC_SEARCH_API_KEY;

Implement language switching

For a single language, it is possible to omit creating the switching part, but this time, we will create it in a non-abbreviated form. First, prepare a language definition file as src/data/locales.ts as follows

TypeScript
export type Language = "en" | "es" | "de" | "it" | "fr" | "zh" | "da" | "ja";

export interface LanguageInfo {
  country: string;
  language: Language;
  label: string;
}

const languages: Record<Language, LanguageInfo> = {
  en: { country: "us", language: "en", label: "English" },
  es: { country: "es", language: "es", label: "Español" },
  de: { country: "de", language: "de", label: "Deutsch" },
  it: { country: "it", language: "it", label: "Italiano" },
  fr: { country: "fr", language: "fr", label: "Français" },
  zh: { country: "cn", language: "zh", label: "中文" },
  da: { country: "dk", language: "da", label: "Dansk" },
  ja: { country: "jp", language: "ja", label: "日本語" },
};

export default languages;

Then provide a drop-down menu to switch languages. We will use React's Context mechanism to provide the language and language change so that another component can see that the language has been switched.

Create a file src/contexts/languageContext.ts defining the context. This file defines the LangageContext as follows

TypeScript
import { createContext } from "react";
import type { Language } from "@/data/locales";

export const LanguageContext = createContext({
  language: "",
  setLanguage: (l: Language) => {},
});

export interface ILanguageContext {
  language: string;
  setLanguage: (t: Language) => void;
}

Prepare as src/components/LocaleSelector/index.tsx using the above definition.

TypeScript
import { useEffect, useContext } from "react";
import languages, { Language } from "@/data/locales";
import { LanguageContext, ILanguageContext } from "@/contexts/languageContext";

export default function LocaleSelector() {
  const { language, setLanguage } =
    useContext<ILanguageContext>(LanguageContext);

  useEffect(() => {
    const savedLanguage = window.localStorage.getItem(
      "lang"
    ) as Language | null;
    if (savedLanguage && languages[savedLanguage]) {
      setLanguage(savedLanguage);
    }
  }, []);

  const handleLanguageChange = (
    event: React.ChangeEvent<HTMLSelectElement>
  ) => {
    const newLanguage = event.target.value as Language;
    setLanguage(newLanguage);

    window.localStorage.setItem("lang", newLanguage);
  };

  return (
    <div>
      <select onChange={handleLanguageChange} value={language || ""}>
        <option value="">Select a language</option>
        {Object.keys(languages).map((key) => (
          <option key={key} value={key}>
            {languages[key as Language].label}
          </option>
        ))}
      </select>
      <p></p>
    </div>
  );
}

Finally, create the src/hooks/useLocalStorage.ts file as the component that makes the browser remember the locale, and code it as follows

TypeScript
import { useState } from "react";

export default function useLocalStorage<T>(
  key: string,
  initialValue: T | (() => T)
): [T, (value: T | ((val: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item =
        typeof window !== "undefined" && window.localStorage.getItem(key);
      return item
        ? JSON.parse(item)
        : typeof initialValue === "function"
        ? (initialValue as () => T)()
        : initialValue;
    } catch (error) {
      return typeof initialValue === "function"
        ? (initialValue as () => T)()
        : initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore =
        value instanceof Function
          ? (value as (val: T) => T)(storedValue)
          : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
  return [storedValue, setValue];
}

The drop-down box is now ready.

Implementation of drop-down menus

First, for a simple implementation, change src/pages/index.tsx on the top page as follows

TypeScript
import Head from "next/head";

export default function Home() {
  return (
    <>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main>
        <h1>Hello Sitecore Search</h1>
      </main>
    </>
  );

Next, since we want to implement this menu this time so that it is available on all pages Update the file src/pages/_app.tsx as follows, using the files we have prepared so far.

TypeScript
import { useEffect, useState } from "react";
import type { AppProps } from "next/app";
import { LanguageContext } from "@/contexts/languageContext";
import useStorage from "@/hooks/useLocalStorage";
import { Language } from "@/data/locales";
import LocaleSelector from "@/components/LocaleSelector";

export default function App({ Component, pageProps }: AppProps) {
  const [storageLanguage, setStorageLanguage] = useStorage(
    "lang",
    "en" as Language
  );
  const [language, setLanguage] = useState<Language>(storageLanguage);

  useEffect(() => {
    setStorageLanguage(language);
  }, [language, setStorageLanguage]);
  return (
    <LanguageContext.Provider value={{ language, setLanguage }}>
      <LocaleSelector />
      <Component {...pageProps} />
    </LanguageContext.Provider>
  );
}

I ran the command npm run dev to get it running and now the drop down box works.

searchsample03.gif

Summary

This time, we have proceeded to the point of preparing an environment in which the widget can be used. In the next issue, we would like to add the widget and display the search results.

Tags