これまでブログでは Sitecore Search SDK を利用して検索結果を表示していました。今回から数回に分けて、Next.js のプレーンな環境に対して Widget を追加していく形で、実際のどういう形で動いているのかを紹介していきます。
Next.js のプロジェクトを作成する
今回はプレーンな Next.js のプロジェクトを作成したいと思います。XM Cloud が提供している Headless SXA でも利用できることを想定して、Pages Router とし、Tailwind.css に関してはデフォルトでオフにします(後日オンにします)。
npx create-next-app
これでサンプルのコードの準備ができました。
プロジェクトにパッケージを追加する
Sitecore Search の SDK のドキュメントを参照すると、Getting Started のページでパッケージのインストールの手順が記載されています。
インストールするのは @sitecore-search/react と @sitecore-search/ui になります。実行したコードは以下の通り。
npm install --save @sitecore-search/react
npm install --save @sitecore-search/ui
続いて Sitecore Search とつなげるための値を .env に追加します。今回は以下のように .env に追加しました。
NEXT_PUBLIC_SEARCH_ENV=
NEXT_PUBLIC_SEARCH_CUSTOMER_KEY=
NEXT_PUBLIC_SEARCH_API_KEY=
NEXT_PUBLIC_SEARCH_PATH=/
上記の値は、Search の Devleoper Resource の画面から確認することができます。値としては、prod, staging, prodEu もしくは apse2 の値を設定することができます。これに関しては、Subdomain ではない場合の URL に記載されている名前を利用してください。
この値を使いやすくするために、constants/search.ts というファイルを作成して、以下のコードを記述しています。
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;
言語切替えの実装
言語に関しては単一言語であれば切り替えの部分を作成するのを省略できますが、今回は省略しない形で作成をしていきます。まず、言語に関する定義ファイルを src/data/locales.ts として以下のように用意します。
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;
続いて言語を切り替えるドロップダウンメニューを用意します。言語が切り替わったことを別のコンポーネントでも確認できるように、今回は React の Context の仕組みを利用して、言語と言語の変更に関して提供するように作ります。
コンテキストの定義を src/contexts/languageContext.ts のファイルを作成します。このファイルは LangageContext を以下のように定義しています。
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;
}
上記の定義を利用して、src/components/LocaleSelector/index.tsx として用意します。
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>
);
}
最後に、ブラウザにロケールを覚えさせるコンポーネントとして、src/hooks/useLocalStorage.ts のファイルを作成して、以下のようにコードを作成します。
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];
}
これでドロップダウンボックスの準備ができました。
ドロップダウンメニューの実装
まず、シンプルな実装にするために、トップページの src/pages/index.tsx を以下のように変更します。
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>
</>
);
続いて、このメニューは全ページで利用できるように今回は実装したいと思いますので src/pages/_app.tsx のファイルを、これまで準備したファイルを利用して以下のように更新します。
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>
);
}
npm run dev とコマンドを実行して動かすと、ドロップダウンボックスが動くようになりました。
まとめ
今回はウィジットを利用できる環境を用意するところまで進めました。次回は、ウィジットを追加して、検索結果を表示するようにしたいと思います。