こんにちは、Sitecore XM Cloud 技術担当の永田です。
今回は、Sitecore XM CloudでGraphQL クエリをコード側で実行するコンポーネントの作成手順をご紹介します。

本記事ではGraphQL クエリをコード側で実行する = フロントエンドアプリ内に GraphQL クエリを記述したファイルを配置し、コンポーネントのコードからクエリを実行してデータを取得することとしています。

公式ドキュメントでは「Use GraphQL to fetch component-level data in JSS Next.js apps」と呼ばれています。

完成イメージはこちらです。
ニュースページとして作成したアイテムの概要やサムネイル等を、GraphQL で日付の順に取得して表示しています。  

完成イメージ

実装したコードと Sitecore アイテムは Github に置いています!
以下のリンクからご覧ください。
Recent コンポーネント ドキュメント

GraphQL クエリをコード側で実行する理由は以下のようなものがあります。

  • クエリの動的な組み立て
    • 例えば検索処理の場合、検索ワードやソートなどのユーザーが指定する部分があるため、コードにより動的にクエリを組み立てる必要があります
  • ISR と組み合わせることにより最新のデータを取得可能
    • 例えば記事ページを取得して表示する記事一覧コンポーネントでは、記事ページのパブリッシュ時に記事一覧コンポーネントも(配置したページはパブリッシュせずに)更新したくなります
    • コンポーネントの tsx ファイルの中にクエリを実行するコードを書いておくことで、ISR によりページを再生成する度に最新のデータを取得できます

なお、この記事は SXA の考え方に基づいています。
そのため、サイトツリーの構造など非 SXA のものと異なる箇所があります。
詳しくは以下のドキュメントをご覧ください。
Using SXA for XM Cloud development | Sitecore Documentation

また、イーストではXM Cloud用に作成した独自のコンポーネントをGitHubで公開中です!
ページ上部か下部のeast-xmcloud-extensionsのバナーからご覧ください。

作成手順

例として TOP ページに置くような、記事ページの最新のものをいくつか取得して表示するコンポーネントを作成します。

データソースとなるアイテムのテンプレートの作成

コンポーネントに表示するデータを定義するためのテンプレートを作成します。

  1. /sitecore/templates/Project/{あなたのサイト} 以下に任意の名前のテンプレートを作成します(例 : Recent)
    • Base Template はTemplates/System/Templates/Standard templateを選択します
    • components フォルダや datasources フォルダ等、わかりやすい名前でフォルダ分けしておくことをお勧めします
  2. Content タブを開き、Base template フィールドで /sitecore/templates/Foundation/Experience Accelerator/StandardValues/_PerSiteStandardValuesテンプレートを継承します
    • サイトごとにスタンダードバリューを設定するためのテンプレートです
  3. Builder タブを開き、必要なフィールドを定義します
    • Root(必須)
      • Type:Droptree
      • Source:query:$site/Home
      • 取得対象の記事ページの親アイテムを指定するためのフィールドです
    • RecentHeader(任意)
      • Type:Rich Text 等
      • コンポーネントの見出しを入力するためのフィールドです
    • ListLink(任意)
      • Type:General Link
      • ニュースの一覧ページ等に遷移するリンクを入力するためのフィールドです
    • その他設定したいフィールドがあれば追加してください
    • 各フィールドの表示名や Title は任意に変更してください

完成イメージはこちらです。  

データソーステンプレートの継承元
データソーステンプレート

フォルダテンプレートの作成

データソースのアイテムを格納するフォルダのテンプレートを作成します。
サイトごとの Data フォルダにこのフォルダを配置することで、その中にデータソースのアイテムを生成できます。

  1. /sitecore/templates/Project/{あなたのサイト} 以下に任意の名前のテンプレートを作成します(例 : Recent Folder)
    • 特に変更が無ければ、/sitecore/templates/Feature/JSS Experience Accelerator/Page Content/Text Folder等の既存のテンプレートを複製することも可能です
    • Base Template はTemplates/System/Templates/Standard templateを選択します
    • detasourceFolder フォルダ等、わかりやすい名前でフォルダ分けしておくことをお勧めします
  2. テンプレートのスタンダードバリューを作成し、挿入オプションにデータソースのアイテムのテンプレートを追加します
    • フォルダを入れ子構造にする場合はこのフォルダのテンプレートも追加します

完成イメージはこちらです。

データソースフォルダテンプレートの継承元
データソースフォルダテンプレート

レンダリングパラメータテンプレートの作成

コンポーネントのスタイル等のパラメータを設定するためのテンプレートを作成します。

  1. /sitecore/templates/Project/{あなたのサイト} 以下に任意の名前のテンプレートを作成します(例 : Recent)
    • 特に変更が無ければ、/sitecore/templates/Feature/JSS Experience Accelerator/Page Content/Rendering Parameters/Rich Text等の既存のテンプレートを複製することも可能です
    • Base Template はTemplates/Foundation/JSS Experience Accelerator/Presentation/Rendering Parameters/BaseRenderingParametersを選択します
    • RenderingParameters フォルダ等、わかりやすい名前でフォルダ分けしておくことをお勧めします
  2. Content タブを開き、Base template フィールドで以下のテンプレートを継承します
    • /sitecore/templates/Foundation/JSS Experience Accelerator/Presentation/Rendering Parameters/BaseRenderingParameters(上記で継承済)
    • /sitecore/templates/Foundation/Experience Accelerator/Dynamic Placeholders/Rendering Parameters/IDynamicPlaceholder
    • /sitecore/templates/Foundation/Experience Accelerator/StandardValues/_PerSiteStandardValues
    • /sitecore/templates/Foundation/Experience Accelerator/Markup Decorator/Rendering Parameters/IRenderingId
  3. Builder タブを開き、パラメータとして設定するデータをフィールドとして定義します
    • PageDisplayCount(必須)
      • Type:Integer
      • 取得する記事の数を指定するためのフィールドです
    • その他設定したいフィールドがあれば追加してください
    • 各フィールドの表示名や Title は任意に変更してください

完成イメージはこちらです。  

レンダリングパラメータテンプレートの継承元
レンダリングパラメータテンプレート

レンダリングアイテムの作成

ページに配置するための Sitecore アイテムを作成します。

  1. /sitecore/layout/Renderings/Project/{あなたのサイト} 以下に Json Rendering アイテムを作成します(例 : Recent)
    • アイテム名は任意ですが、この後作成する tsx ファイルの名前と揃えることをお勧めします
      • 揃えなかった場合、Component Name フィールドに tsx ファイルの名前を入力してください
    • Other properties フィールドに以下のプロパティを追加します
      • IsRenderingsWithDynamicPlaceholders:true
    • Parameters Template フィールドに作成したレンダリングパラメータテンプレートを設定します
    • Datasource Location フィールドにデータソースのアイテムを格納する場所やクエリを設定します
      • 例:query:$site/*[@@name='Data']/*[@@templatename='Recent Folder']|query:$sharedSites/*[@@name='Data']/*[@@templatename='Recent Folder']
    • Datasource Template フィールドにデータソースのアイテムのテンプレートを設定します

完成イメージはこちらです。

レンダリングアイテム
OtherProperties
ParametersTemplate
Datasource

フォルダ等その他必要なアイテムの作成

その他必要なアイテムを作成します。

  1. {あなたのサイト}/Data に作成したフォルダテンプレートを使って、データソースのフォルダを作成します
    フォルダ
  2. {あなたのサイト}/Presentation/Available Renderings に任意の名前で Available Renderings を追加します(MyComponents 等)
  3. 上記の Available Renderings の Rendrings フィールドに作成したコンポーネントを追加します
    Available Renderings
  4. {あなたのサイト}/Presentation/Headless Variants にコンポーネントの名前の Variants を追加します
  5. 上記の子アイテムに Default という名前の Variant を作成します
    Variant

GraphQL クエリ用 ファイルの作成

以下は「指定したアイテム配下のレイアウトを持つページ」を取得するクエリ例です。
フロントエンドアプリの \src{あなたのプロジェクト}\src\graphql フォルダに、ts ファイル(例:RecentQuery.dynamic.graphql.ts)を作成します。
ページから取得するフィールドは任意に変更してください。

export const RecentQuery = `query getRecent ($rootId: String!, $language: String!, $pageDisplayCount: Int!) {
  search(
    where: {
      AND: [
        {
          name: "_language",
          value: $language,
          operator: EQ
        },
        {
          name: "_hasLayout",
          value: "true",
          operator: EQ
        },
        {
          name: "_parent",
          value: $rootId,
          operator: CONTAINS
        }
      ]
    }
    orderBy: {
      name: "ReleaseDate"
      direction: DESC
    }
    first: $pageDisplayCount
  ) {
    results {
      id
      Title: field(name: "Title") {
        jsonValue
      }
      Thumbnail: field(name: "Thumbnail") {
        ...on ImageField {
          jsonValue
        }
      }
      Overview: field(name: "Overview") {
        jsonValue
      }
      ReleaseDate: field(name: "ReleaseDate") {
        ...on DateField {
          jsonValue
        }
      }
      url {
        path
      }
    }
  }
}`;

tsx ファイルの作成

続いて、GraphQL クエリを実行するためのコンポーネントのコード例です。
フロントエンドアプリの \src{あなたのプロジェクト}\src\components フォルダに、コンポーネントの tsx ファイル(例:Recent.tsx)を作成します。
ファイル名はレンダリングアイテムの Component Name フィールドとそろえてください。

getStaticProps / getServerSideProps の中にクエリを実行するコードを書いておくことで、ページを再生成する度に最新のデータを取得できます。

なお、こちらのコードは JSS ver22.3.1 時点のものです。
また、データが無かった場合の例外処理などは含まれていないため、ご自身のプロジェクトの要件に合わせて追加してください。

import {
  useSitecoreContext,
  Text,
  TextField,
  Link as JssLink,
  LinkField,
  Image as JssImage,
  ImageField,
  RichText,
  RichTextField,
  DateField,
  Item,
  GetStaticComponentProps,
} from "@sitecore-jss/sitecore-jss-nextjs";
import { RecentQuery } from "src/graphql/RecentQuery.dynamic.graphql";
import graphqlClientFactory from "lib/graphql-client-factory";
import config from "temp/config";
import Link from "next/link";

interface RcentItemFields {
  Title: { jsonValue: TextField };
  Thumbnail: { jsonValue: ImageField };
  Overview: { jsonValue: RichTextField };
  ReleaseDate: { jsonValue: { value?: string; editable?: string } };
}

type RecentItemProps = {
  id: string;
  fields: RcentItemFields;
  url: string;
};

interface Fields {
  RecentHeader: RichTextField;
  ListLink: LinkField;
}

interface RecentProps {
  params: { [key: string]: string };
  fields: Fields;
  articles: RecentItemProps[];
}

interface RawItemData {
  id: string;
  Title: { jsonValue: TextField };
  Thumbnail: { jsonValue: ImageField };
  Overview: { jsonValue: RichTextField };
  ReleaseDate: { jsonValue: { value?: string; editable?: string } };
  url: {
    path: string;
  };
}

type SearchQueryData = {
  search: {
    results: RawItemData[];
  };
};

type ComponentContentProps = {
  pageState: string | undefined;
  url: string;
  children: JSX.Element;
};

const RecentItem = (props: ComponentContentProps): JSX.Element => {
  return (
    <li className="item">
      {props.pageState !== "edit" ? (
        <Link href={props.url}>{props.children}</Link>
      ) : (
        <>{props.children}</>
      )}
    </li>
  );
};

export const Default = (props: RecentProps): JSX.Element => {
  const { sitecoreContext } = useSitecoreContext();
  const id = props.params.RenderingIdentifier;
  return (
    <div
      className={`component recent ${props.params.styles}`}
      id={id ? id : undefined}
    >
      <div className="component-content">
        <div className="recent-header">
          <RichText field={props.fields.RecentHeader} />
        </div>
        <ul>
          {props.articles &&
            Array.isArray(props.articles) &&
            props.articles.map((item) => (
              <RecentItem
                key={item.id}
                pageState={sitecoreContext.pageState}
                url={item.url}
              >
                <>
                  <div className="item-content-first">
                    <JssImage field={item.fields?.Thumbnail?.jsonValue} />
                  </div>
                  <div className="item-content-second">
                    <div className="item-title">
                      <DateField
                        field={item.fields?.ReleaseDate?.jsonValue}
                        render={(date) => date?.toLocaleDateString()}
                      />{" "}
                      <Text field={item.fields?.Title?.jsonValue} />
                    </div>
                    <div className="item-overview">
                      <RichText field={item.fields?.Overview?.jsonValue} />
                    </div>
                  </div>
                </>
              </RecentItem>
            ))}
        </ul>
        <div className="field-recentlink">
          <JssLink field={props.fields.ListLink} />
        </div>
      </div>
    </div>
  );
};

export const getStaticProps: GetStaticComponentProps = async (
  rendering,
  layoutData
) => {
  try {
    const context = layoutData?.sitecore?.context;
    const route = layoutData?.sitecore?.route;
    const params = rendering?.params;
    const fields = rendering?.fields;

    if (!context || !route || !params || !fields) {
      return { articles: [] as RecentItemProps[] };
    }
    const graphQLClient = graphqlClientFactory();

    const language = context.language ?? config.defaultLanguage;

    const pageDisplayCount = Number(params.PageDisplayCount);
    if (
      pageDisplayCount &&
      Number.isInteger(pageDisplayCount) &&
      pageDisplayCount < 1
    ) {
      return { articles: [] as RecentItemProps[] };
    }

    const rootItem = fields.Root as Item;
    if (!rootItem) {
      return { articles: [] as RecentItemProps[] };
    }
    const rootId = rootItem.id;

    const data = await graphQLClient.request<SearchQueryData>(RecentQuery, {
      rootId,
      language,
      pageDisplayCount,
    });

    const articles: RecentItemProps[] =
      data.search?.results?.map((d) => {
        return {
          id: d.id,
          fields: {
            Title: d.Title,
            Thumbnail: d.Thumbnail,
            Overview: d.Overview,
            ReleaseDate: d.ReleaseDate,
          },
          url: d.url?.path,
        };
      }) ?? [];

    return { articles };
  } catch (e) {
    console.error(e);
    return { articles: [] as RecentItemProps[] };
  }
};

使用イメージ

Page builder でコンポーネントを配置すると、次のように表示されます。  

コンポーネントの配置

見出しとリンクの入力欄は任意に編集してください。  
また、Root フィールドは Page builder またはコンテンツ エディタから設定することができます。

トラブルシューティング

もしもコンポーネントを配置したページが 500 エラーになってしまう場合、tsx ファイルのデータの受け取り方、またはその型定義が間違っている可能性が非常に高いです。
その時は、以下の手順で確認することができます。

  1. tsx ファイルの Default 関数の中身を全てコメントアウトし、以下のコードを追加します
    console.log(props);
    return <p>My Component</p>;
    
  2. 500 エラーが出ているページを更新し開発者ツールを開きます
  3. コンポーネントが受け取る props オブジェクトがコンソールに表示されるので、データの構造や型が合っているか確認します

以上で、手順は完了です。
ここまでお読みいただきありがとうございました。

参考ドキュメント

このコンポーネント以外にも、イーストではXM Cloud用に作成した独自のコンポーネントをGitHubで公開中です!
以下のeast-xmcloud-extensionsのバナーからご覧ください。