コンテンツにスキップ

WordPress CMS統合ガイド

このドキュメントは、Next.js StorefrontとWordPress CMSの統合方法、アーキテクチャ、実装パターンを説明します。

概要

このプロジェクトでは、WordPressをHeadless CMSとして使用し、以下のコンテンツを管理します:

  • お知らせ(Announcements): サイト全体のお知らせ、重要なお知らせの固定表示
  • キャンペーン(Campaigns): プロモーションキャンペーン情報
  • Heroスライド(Hero Slides): ホームページのヒーローセクション用スライド

Next.js Storefrontは、WordPressのGraphQL API(WPGraphQL)を通じてコンテンツを取得し、サーバーサイドレンダリング(SSR)で表示します。

レンダリング責務の定義

StorefrontとWordPressのレンダリング責務を以下のように定義します。これはマーケティング部が自由度を持ってストアを運用できるようにするため、特にコンテンツページにおいてWordPress側の表現力を最大限に活かすことを目的としています。

1. サイト外枠(枠組み)

  • 担当: Storefront (Next.js)
  • 対象: ヘッダー、フッター、グローバルナビゲーション
  • 理由: カート状態、ログイン、検索などECのコアロジックを伴うため。
  • 管理: WordPressのメニュー機能でリンク構造を管理し、StorefrontがネイティブReactコンポーネントとして描画します。

2. メインコンテンツ(中身)

  • 担当: WordPress
  • 対象: 固定ページ、お知らせ詳細、キャンペーン詳細、商品リッチ説明
  • 理由: Elementor等のページビルダーによる自由なレイアウトを可能にするため。
  • 実装: WordPressが生成するHTMLを正とし、Storefrontはそれをセーフガード付きで描画します。

3. 動的セクション(ハイブリッド統合)

  • 担当: Storefront × WordPress
  • 対象: おすすめ商品グリッド、最新お知らせリスト、ヒーローカルーセル
  • 実装: 「セクション・ハイドレーション」方式を採用。WordPress内の特定のマーク(ショートコード等)を検出し、Storefront側のReactコンポーネントに実行時に差し替えます。

高度な統合メカニズム

WordPressの表現力とStorefrontの機能性を両立させるための主要な仕組みです。

1. アセット同期 (Asset Synchronization)

WordPress側のプラグインやビルダー(Elementor等)が生成するCSS/JSをStorefront側で自動的に読み込みます。

  • WordPress側: カスタムプラグインで enqueuedAssets フィールドをGraphQLに公開。
  • Storefront側: WordPressAssetLoader コンポーネントが、取得したURLを元に <link><script> をページに挿入します。
  • 対象: Gutenberg標準ブロック、Elementor共通アセット、ページ固有のCSSなど。

2. コンテンツ・プロセッサー (Content Processor)

WordPressから取得したHTMLを生のまま表示せず、Storefront向けに最適化します。

  • リンク変換: WordPressの絶対URL(例: https://wp.example.com/pages/about)を、Storefrontの相対パス(例: /pages/about)に自動置換します。
  • 画像最適化: 全ての画像に loading="lazy" を自動付与し、パフォーマンスを向上させます。
  • 実装: apps/storefront/src/lib/cms/wordpress/content-processor.ts

3. セクション・ハイドレーション (Section Hydration)

WordPressのレイアウトの途中に、Storefrontの動的Reactコンポーネントを埋め込みます。

  • ショートコード: [ritsubi_section type="Announcements" title="最新ニュース"]
  • ハイドレーター: WordPressSectionHydrator がHTMLを解析し、data-ritsubi-section 属性を持つ要素を本物のReactコンポーネントに差し替えます。
  • 運用: マーケティング担当者は、ビルダーの好きな場所にショートコードを置くだけでEC機能を呼び出せます。

4. CSS セーフガード (CSS Safeguards)

WordPress側のスタイルがStorefront全体のデザインを壊さないための保護機能です。

  • スコープ: WordPressコンテンツは必ず .wp-content クラスを持つ要素でラップします。

  • 詳細度制御: Tailwind Typography (prose) のデフォルト設定よりも、WordPress側のインライン指定や独自CSSが優先されるよう、CSSレイヤー (@layer base) で調整を行っています。

  • リセット回避: TailwindのPreflightによる強力なリセット(画像のマージン消去など)を、.wp-content 内ではWordPress標準に近い挙動に戻します。

5. セキュア・プレビュー認証バイパス (Secure Preview Bypass)

B2Bサイトの特性上、通常は全ページに認証(ログイン)が必要ですが、WordPressでの編集体験を損なわないよう、プレビュー時のみ安全に認証をスキップする仕組みを備えています。

  • 仕組み: 共有シークレット方式

  • WordPressとStorefrontの両方に共通の秘密鍵 STOREFRONT_PREVIEW_SECRET を設定します。

  • WordPressの「プレビュー」ボタンが生成するURLに、自動的に &secret={秘密鍵} が付与されます。

  • StorefrontのMiddlewareがこの秘密鍵を検証し、一致する場合に限りログイン画面へのリダイレクトをスキップします。

  • メリット:

  • 編集者はログインの手間なく、即座に実環境での見た目を確認できます。

  • URLに推測困難な秘密鍵が含まれるため、第三者による意図しない閲覧を防ぎます。

  • 設定:

  • シークレットはDopplerで管理され、dev, stg, prd の各環境で同期されています。


ACF (Advanced Custom Fields) 統合

ACFは、WordPressの自由なレイアウトの中に「ECの動的データ」を安全かつ正確に流し込むための設定インターフェースとして機能します。

1. 設計思想:データとレイアウトの分離

役割 担当ツール 内容
位置 (Where) ページビルダー セクションの配置順序、背景色、外側の余白
属性 (What) ACF 表示する商品カテゴリ、件数、独自タイトルなどの設定値
描画 (How) Storefront (React) ACFの設定値を props として受け取り、最新データを描画

2. 動的セクションの共通フィールド定義 (Schema)

[ritsubi_section] ショートコードや ACF Block で使用する標準的なフィールド名は以下の通りです。これらは Storefront 側の React コンポーネントにそのままプロパティとして渡されます。

フィールド名 (Slug) タイプ 説明 React側 Props 名
section_type Select セクションの種類 (Hero, Announcements, Products等) type
section_title Text セクションの見出し文字 title
section_eyebrow Text タイトル上の小見出し eyebrow
display_limit Number 表示するアイテムの最大件数 limit
target_category Select/Text 取得対象のカテゴリ名またはスラッグ category
is_featured_only True/False おすすめ/注目アイテムのみに絞り込むか featuredOnly

3. 運用フロー

  1. 開発者: Storefront 側に新しい React コンポーネント(例:ProductRanking)を作成し、SECTION_COMPONENTS に登録する。
  2. 開発者: WordPress の ACF 側で、section_type の選択肢にそのコンポーネント名を追加する。
  3. マーケ部: ビルダー上でセクションを配置し、ACF のパネルから「表示件数」や「カテゴリ」を選択する。

これにより、プログラムのコードを触ることなく、マーケティング施策に合わせた高度な EC ページの構築が可能になります。


アーキテクチャ

┌─────────────────┐
│ WordPress CMS   │
│ (Headless)      │
│                 │
│ - Announcements │
│ - Campaigns     │
│ - Hero Slides   │
└────────┬────────┘
         │
         │ GraphQL API
         │ (WPGraphQL)
         │
         ▼
┌─────────────────┐
│ Next.js         │
│ Storefront      │
│                 │
│ - SSR Fetch     │
│ - Components    │
│ - Display       │
└─────────────────┘

データフロー

  1. コンテンツ作成: WordPress管理画面でコンテンツを作成・編集
  2. GraphQL API: WPGraphQLプラグインがGraphQLエンドポイントを提供
  3. Next.js Fetch: StorefrontがサーバーサイドでGraphQL APIを呼び出し
  4. 表示: 取得したデータをReactコンポーネントで表示

設定

環境変数

Next.js Storefrontでは以下のルールでエンドポイントを解決します。

  1. サーバー実行文脈(SSR/Route Handler等): WORDPRESS_GRAPHQL_ENDPOINT (例: https://wp.example.com/graphql)を最優先で参照し、WordPress本番ドメインへ直接アクセスする。
  2. クライアント実行文脈: WordPress の実ドメインを公開しないためプロキシ /api/wordpress/graphql を使用する。プロキシは apps/storefront/src/app/api/wordpress/graphql/route.ts で実装されており、storefrontConfig からは siteUrl + /api/wordpress/graphql の絶対URLとして参照される。

結果として、秘密情報は環境変数に閉じ込めつつ、クライアントは統一したパスでアクセスできます。

設定ファイル

設定は apps/storefront/src/lib/config.ts で管理されています:

export const storefrontConfig = {
  wordpressGraphqlEndpoint:
    typeof window === 'undefined'
      ? (process.env.WORDPRESS_GRAPHQL_ENDPOINT ??
        new URL('/api/wordpress/graphql', siteUrl).toString())
      : new URL('/api/wordpress/graphql', siteUrl).toString(),
  // ...(Vendure関連設定など)
};

※実装は apps/storefront/src/lib/config.ts にあり、プロキシ/直接アクセスの切り替えが自動化されています。

実装パターン

GraphQLクライアント

WordPress GraphQL APIへのリクエストは apps/storefront/src/lib/cms/wordpress/client.tsfetchWordPressGraphQL 関数を使用します。

import { fetchWordPressGraphQL } from '@/lib/cms/wordpress/client';

const data = await fetchWordPressGraphQL<QueryResult>({
  query: MY_QUERY,
  variables: { first: 10 },
  cache: 'no-store', // キャッシュ設定
  allowPartialData: true, // エラーがあってもdataを返す
});

コンテンツタイプ別の実装

お知らせ(Announcements)

ファイル: apps/storefront/src/lib/cms/wordpress/announcements.ts

主な機能:

  • fetchAnnouncements(): お知らせ一覧を取得
  • fetchFeaturedAnnouncements(): フィーチャー済み(isFeatured: true)のお知らせを取得

使用例:

import {
  fetchAnnouncements,
  fetchFeaturedAnnouncements,
} from '@/lib/cms/wordpress/announcements';

// 通常のお知らせ一覧
const announcements = await fetchAnnouncements({ limit: 10 });

// フィーチャー済みお知らせ(サイト上部に固定表示)
const featured = await fetchFeaturedAnnouncements({ limit: 5 });

コンポーネント:

  • FeaturedAnnouncementBanner: フィーチャー済みお知らせをサイト上部に表示

  • AnnouncementSection: お知らせ一覧セクション

キャンペーン(Campaigns)

ファイル: apps/storefront/src/lib/cms/wordpress/campaigns.ts

主な機能:

  • fetchCampaigns(): キャンペーン一覧を取得(ACFのorderフィールドでソート)

使用例:

import { fetchCampaigns } from '@/lib/cms/wordpress/campaigns';

const campaigns = await fetchCampaigns({ limit: 10 });

コンポーネント:

  • CampaignBannerSection: キャンペーンバナー一覧表示

Heroスライド(Hero Slides)

ファイル: apps/storefront/src/lib/cms/wordpress/hero-slides.ts

主な機能:

  • fetchHeroSlides(): Heroスライド一覧を取得(ACFのorderフィールドでソート)

使用例:

import { fetchHeroSlides } from '@/lib/cms/wordpress/hero-slides';

const heroSlides = await fetchHeroSlides({ limit: 10 });

コンポーネント:

  • HomeHero: ホームページのHeroセクション(カルーセル表示)

エラーハンドリング

フォールバッククエリ

各コンテンツタイプは、メインクエリが失敗した場合にフォールバッククエリを実行します:

// メインクエリ(announcements)
try {
  const data = await fetchWordPressGraphQL({ query: ANNOUNCEMENT_QUERY });
  // ...
} catch (error) {
  // フォールバッククエリ(contentNodes)
  const fallback = await fetchWordPressGraphQL({
    query: ANNOUNCEMENT_FALLBACK_QUERY,
  });
  // ...
}

部分データの許可

allowPartialData: true を設定すると、GraphQLエラーがあってもdataが存在する場合はデータを返します:

const data = await fetchWordPressGraphQL({
  query: MY_QUERY,
  allowPartialData: true, // エラーがあってもdataを返す
});

サーバーサイドレンダリング(SSR)

すべてのWordPressコンテンツはサーバーサイドで取得されます:

ページコンポーネント

// app/page.tsx
export default async function HomePage() {
  const announcements = await fetchAnnouncements({ limit: 10 });
  const heroSlides = await fetchHeroSlides({ limit: 10 });
  const campaigns = await fetchCampaigns({ limit: 10 });

  return (
    <HomePageClient
      initialAnnouncements={announcements}
      initialHeroSlides={heroSlides}
      initialCampaigns={campaigns}
    />
  );
}

レイアウトコンポーネント

// app/layout.tsx
export default async function RootLayout({ children }: RootLayoutProps) {
  const featuredAnnouncements = await fetchFeaturedAnnouncements({ limit: 5 });

  return (

    <html>
      <body>
        {featuredAnnouncements.length > 0 && (
          <FeaturedAnnouncementBanner announcements={featuredAnnouncements} />
        )}
        {/* ... */}

      </body>
    </html>
  );
}

キャッシュ戦略

現時点では、WordPressコンテンツはキャッシュしない設定(cache: 'no-store')になっています:

  • 理由: コンテンツ更新を即座に反映するため
  • 将来の改善: ISR(Incremental Static Regeneration)やrevalidate設定を検討

コンテンツモデル

お知らせ(Announcement)

WordPress投稿タイプ: announcement

標準フィールド:

  • title: タイトル
  • date: 公開日

  • slug: スラッグ

  • link: リンクURL

ACFフィールド (announcementMeta):

  • isFeatured: フィーチャー済みフラグ(boolean)

  • relatedLink: 関連リンク(string)

キャンペーン(Campaign)

WordPress投稿タイプ: campaign

標準フィールド:

  • title: タイトル
  • date: 公開日
  • slug: スラッグ
  • link: リンクURL
  • featuredImage: アイキャッチ画像

ACFフィールド (campaignMeta):

  • campaignId: キャンペーンID(string)
  • order: 表示順序(number)
  • image: キャンペーン画像(MediaItem)

連携ルール:

  • campaignId は Vendure 側のキャンペーンIDと一致させる
  • URLに使用できない文字は避ける(英字・数字・ハイフン推奨)
  • URLパスは大文字/小文字を区別する環境があるため、小文字に統一する
  • StorefrontはURLパスを小文字に正規化し、正規URLへリダイレクトする
  • campaignId^[a-z0-9-]+$ を満たす
  • 顧客向けの表示内容(名称・説明・画像)はWordPress側で管理する

Storefront側の正規化仕様(キャンペーンページ):

  • キャンペーンページのURLは /campaigns/{campaignId} を正とする
  • 受け取った campaignId を小文字化して検索する
  • 大文字混在のURLでアクセスされた場合は、正規URLへリダイレクトする
  • 例: /campaigns/SMNR-0001/campaigns/smnr-0001
  • WordPressの slug とは独立して campaignId で検索する
  • WordPressのプレビューは slug ベースのため、原則 slug = campaignId とする
  • 顧客に campaignId を見せたくない場合のみ、slug を別名にする
  • 既存の /campaigns/{slug} 形式は廃止し、/campaigns/{campaignId} に統一する

Storefront実装方針(キャンペーン詳細):

  • ルーティング: apps/storefront/src/app/(site)/campaigns/[campaignId]/page.tsx
  • ページ内で campaignId を小文字化し、差異があれば redirect で正規URLへ誘導
  • campaignId をキーに WordPress からキャンペーン詳細を取得する
  • /campaigns/[slug] 実装は廃止する

Heroスライド(Hero Slide)

WordPress投稿タイプ: hero_slide

標準フィールド:

  • title: タイトル
  • date: 公開日
  • slug: スラッグ
  • link: リンクURL

ACFフィールド (heroSlideMeta):

  • order: 表示順序(number)
  • media: メディア(image/video)
  • type: メディアタイプ('image' | 'video')
  • image: 画像(desktop/mobile対応)
  • video: 動画(desktop/mobile対応)
  • alt: altテキスト
  • link: リンク先(Campaign/Announcement)

トラブルシューティング

GraphQLエラーが発生する

  1. エンドポイントが正しいか確認
curl http://localhost:8181/graphql -X POST -H "Content-Type: application/json" -d '{"query":"{ __typename }"}'
  1. WPGraphQLプラグインが有効化されているか確認
docker compose run --rm wp-cli plugin list
  1. ACFフィールドがGraphQLに公開されているか確認
  2. WordPress管理画面 → Custom Fields → フィールドグループ → GraphQL設定

コンテンツが表示されない

  1. 投稿が公開状態か確認
  2. GraphQLスキーマに投稿タイプが登録されているか確認
query {
  __schema {
    types {
      name
    }
  }
}
  1. ブラウザの開発者ツールでネットワークエラーを確認
  2. サーバーログでエラーを確認

画像が表示されない

  1. Next.jsの画像設定を確認next.config.js
  2. remotePatternsにWordPressドメインが登録されているか確認
  3. 画像URLが正しいか確認

参考資料