【Sitecore 連載 第4回】JSS と Next.js で組む headless 開発実践
CMS Sitecore

【Sitecore 連載 第4回】JSS と Next.js で組む headless 開発実践

公開: 更新: 約5分で読めます

はじめに

前回(第 3 回「Personalize と CDP で実装する 1to1 体験」)では、配信の上に乗るパーソナライゼーション層を見た。本稿(第 4 回)は開発側の話に戻る。Sitecore 開発と言えば、長らく Helix 三層構造に Glass Mapper、MVC controller rendering、Solr インデックスという .NET Framework 系のスタックを意味していた。XM Cloud 中心の現在は、ここがほぼ別物になっている。フロントエンドは Next.js(App Router)+ Sitecore JSS、データ取得は GraphQL、デプロイは Vercel や Azure Static Web Apps、コンテンツモデルは Sitecore CLI で Git 管理。本稿ではその実装の中身を順に追う。

プロジェクト構成の全体像

典型的な XM Cloud + Next.js プロジェクトは、ひとつの monorepo に「コンテンツ定義(YAML)」と「フロントエンド(Next.js アプリ)」が同居する構成を取ることが多い。Sitecore CLI で扱うコンテンツ側は src/items(テンプレート、レンダリング定義、サイト設定)に YAML として落とし、フロントエンドは src/app(App Router)配下に React コンポーネントとして書く。

my-sitecore-site/
├ src/
│ ├ items/                 # Sitecore CLI が同期するテンプレート等
│ │ ├ templates/
│ │ └ renderings/
│ └ app/                   # Next.js App Router
│   ├ [[...path]]/         # 全パスをキャッチして Sitecore に問い合わせ
│   │ └ page.tsx
│   └ api/editing/         # XM Cloud Pages からの編集 webhook
├ components/              # Renderings / Components の実装
├ lib/sitecore/            # Layout Service クライアント
└ sitecore.json

Layout Service と動的ルーティング

Browser リクエストから Next.js が Layout Service を呼び、JSS Placeholder が React コンポーネントを並べるまでのフロー図
リクエストが入ってから JSS の Placeholder が React コンポーネントを差し込むまでの流れ。Layout Service はサイト構造と rendering を JSON で返す。

headless Sitecore の根幹は Layout Service だ。あるパスをリクエストすると、そのページに配置された rendering(コンポーネント)の一覧と、各 rendering へのデータソース、サイト構造を JSON で返す。Next.js 側はこれを取って、対応する React コンポーネントを並べる。App Router であれば app/[[...path]]/page.tsx ですべての URL をキャッチし、サーバ側で Layout Service を叩く形になる。

// app/[[...path]]/page.tsx
import { getLayoutData } from "@/lib/sitecore/layout";
import { Placeholder } from "@sitecore-jss/sitecore-jss-nextjs";

export default async function Page({ params }: { params: { path?: string[] } }) {
  const path = "/" + (params.path?.join("/") ?? "");
  const layout = await getLayoutData(path, "ja-JP");

  if (!layout.sitecore.route) return notFound();

  return (
    <Placeholder name="main" rendering={layout.sitecore.route} />
  );
}

Placeholder は JSS が提供するヘルパで、Layout Service が返した rendering の配列をたどり、登録されている React コンポーネントを呼ぶ。コンポーネントとレンダリング名のマッピングはアプリ起動時にレジストリへ登録しておく。

Component の実装

個々のコンポーネントは普通の React 関数コンポーネントとして書ける。データソースは fields として props に入ってくる。フィールドの種別(テキスト、画像、リンク)に応じた専用の表示コンポーネントが JSS から提供されており、編集時にインライン編集できるようになっている。

import { Text, Image, Link } from "@sitecore-jss/sitecore-jss-nextjs";

type HeroFields = {
  Title:    { value: string };
  Subtitle: { value: string };
  Cover:    { value: { src: string; alt: string } };
  CtaLink:  { value: { href: string; text: string } };
};

export default function Hero({ fields }: { fields: HeroFields }) {
  return (
    <section className="hero">
      <Image field={fields.Cover} className="hero-cover" />
      <div className="hero-body">
        <h1><Text field={fields.Title} /></h1>
        <p><Text field={fields.Subtitle} /></p>
        <Link field={fields.CtaLink} className="hero-cta" />
      </div>
    </section>
  );
}

TextImage を直書きの <p>{fields.Title.value}</p> に置き換えても普通に動くが、こうしておくと XM Cloud Pages のビジュアルエディタから直接編集できるようになる。フロントの実装そのものが編集体験に直結している点が、headless Sitecore の特徴だ。

SSG と ISR の使い分け

Next.js App Router は、デフォルトで静的化を試みる。コンテンツが頻繁に変わらないサイト(コーポレートサイト、製品紹介サイト)は素直に SSG(Build-time generation)でよい。Sitecore 側で公開操作が走ったら、Experience Edge から webhook を受けて該当ページだけ ISR 再生成する仕組みが標準的だ。

// app/api/revalidate/route.ts — Edge から呼ばれる webhook
import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const signature = req.headers.get("x-sitecore-signature");
  if (!verifySignature(signature, await req.text())) {
    return NextResponse.json({ ok: false }, { status: 401 });
  }
  const body = await req.json();
  for (const path of body.changedPaths) {
    revalidatePath(path);
  }
  return NextResponse.json({ ok: true });
}

逆に、ニュースサイトやキャンペーン LP のように分単位で内容が変わるケースは Edge SSR(each request → Edge から GraphQL)を選ぶ。Vercel や Azure Front Door のエッジで実行すれば、レイテンシも問題になりにくい。

編集モードと preview rendering host

XM Cloud Pages からの編集を有効にするには、フロントエンドが editing host モードでも動くように作る必要がある。実体は、Sitecore 側からのリクエストに対して preview endpoint からデータを取り、編集用の追加 attributes(rendering ID など)を吐き出す挙動を切り替えることだ。JSS が提供する middleware にほぼ任せられる。

// middleware.ts
import { editingMiddleware } from "@sitecore-jss/sitecore-jss-nextjs/middleware";
export default editingMiddleware({
  apiKey: process.env.SITECORE_API_KEY!,
});
export const config = { matcher: ["/api/editing/:path*"] };

CI/CD

典型的なパイプラインは、GitHub Actions で 3 つのジョブを走らせる構成になる。「コンテンツ定義の同期(Sitecore CLI で ser push)」「フロントエンドのビルドとデプロイ(Vercel / Azure SWA)」「Personalize / CDP の Decision Model 同期(API 経由)」。コンテンツ側とフロント側の更新が独立しているため、デプロイの粒度を分けられる。

XP からの移行で押さえるべき点

既存の Helix プロジェクトをそのまま JSS へ持ち込むのはほぼ不可能で、コンポーネント単位での再実装になる。一方、テンプレートとコンテンツモデルの設計はかなりの割合で持ち越せる。データの移行は Sitecore CLI と XM Cloud Migration Toolkit を組み合わせて、テンプレ・アイテム単位で段階的に移すパターンが現実的だ。Glass Mapper のような ORM 層は JSS の世界では不要になる(GraphQL の型生成がほぼその役割を果たす)。

次回

次回(第 5 回)は 「composable 構成の性能設計 — Edge 配信と CDN 最適化」。ここまで組み上げた構成を性能の観点から見直し、Experience Edge と CDN の TTL 制御、Next.js の ISR、GraphQL persisted query、Edge ログによる Real User Monitoring を扱う。