# 機能設計書 62-CMSページ表示

## 概要

本ドキュメントは、Shopware StorefrontにおけるCMS（Content Management System）で作成されたページを表示する機能について、その処理内容、入出力仕様、およびコード構造を記述した機能設計書である。

### 本機能の処理概要

本機能は、管理画面のショッピング体験（Shopping Experiences）で作成されたCMSレイアウトをストアフロントで表示する機能を提供する。

**業務上の目的・背景**：ECサイトでは商品ページ以外にも、ブランドストーリー、キャンペーン情報、ヘルプページなど多様なコンテンツページが必要となる。CMSページ表示機能により、技術者でなくても管理画面からドラッグ&ドロップでページを作成し、ストアフロントに公開できる。これによりマーケティング施策の迅速な展開とサイト運用コストの削減を実現する。

**機能の利用シーン**：
- ウィジェットとしてCMSレイアウトを既存ページに埋め込む場合（`/widgets/cms/{id}`）
- フルページとしてCMSレイアウトを独立表示する場合（`/page/cms/{id}`）
- カテゴリに紐付いたCMSレイアウトをナビゲーション経由で表示する場合（`/widgets/cms/navigation/{navigationId}`）
- 商品一覧のフィルター情報を取得する場合
- 商品詳細のバリアント切り替え時にBuy Boxを更新する場合

**主要な処理内容**：
1. CMSページIDまたはナビゲーションIDを受け取る
2. Store API経由でCMSページデータを取得
3. CMSページに含まれるブロック・スロットの解決
4. 商品リスティング要素がある場合は商品データを読み込み
5. Twigテンプレートでレンダリング

**関連システム・外部連携**：Store APIのCmsRoute、CategoryRoute、ProductListingRoute、ProductDetailRoute、FindProductVariantRoute

**権限による制御**：公開ページは認証不要。非公開設定のページは管理画面での制御

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 26 | CMSページ（ウィジェット） | 主機能 | CMSレイアウトの単体表示（CmsController） |
| 27 | CMSページ（フル） | 主機能 | CMSレイアウトのフルページ表示 |
| 42 | CMS管理 | 主機能 | ショップページのレイアウト編集 |
| 43 | メディア管理 | 補助機能 | 画像・ファイルのアップロード・管理 |

## 機能種別

データ連携 / 画面表示

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes* | CMSページのUUID | 有効なUUID形式 |
| navigationId | string | Yes* | ナビゲーション（カテゴリ）のUUID | 有効なUUID形式 |
| slots | string\|array | No | 取得するスロットIDのリスト | パイプ区切りまたは配列 |
| request | Request | Yes | HTTPリクエストオブジェクト | - |
| salesChannelContext | SalesChannelContext | Yes | 販売チャネルコンテキスト | - |

*idまたはnavigationIdのいずれかが必須

### 入力データソース

- リクエストURL: CMSページIDまたはナビゲーションID
- Store API: AbstractCmsRoute、AbstractCategoryRoute
- データベース: cms_page、cms_section、cms_block、cms_slot各テーブル

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| cmsPage | CmsPageEntity | CMSページエンティティ |
| cmsPage.sections | CmsSectionCollection | セクションのコレクション |
| cmsPage.sections.blocks | CmsBlockCollection | ブロックのコレクション |
| cmsPage.sections.blocks.slots | CmsSlotCollection | スロットのコレクション |

### 出力先

- ウィジェット: `@Storefront/storefront/page/content/detail.html.twig`
- フルページ: `@Storefront/storefront/page/content/index.html.twig`
- レスポンスヘッダー: `x-robots-tag: noindex`（ウィジェット時）

## 処理フロー

### 処理シーケンス

```
1. CmsController がリクエストを受信
   └─ page(), pageFull(), category() のいずれか

2. パラメータのバリデーション
   └─ id または navigationId が必須

3. Store API経由でCMSページを取得
   ├─ page()/pageFull(): AbstractCmsRoute::load()
   └─ category(): AbstractCategoryRoute::load()

4. CmsPageLoadedHookの実行
   └─ App Scriptsによる拡張ポイント

5. Twigテンプレートのレンダリング
   └─ detail.html.twig または index.html.twig

6. レスポンスヘッダーの設定
   └─ ウィジェットの場合は x-robots-tag: noindex を設定

7. レスポンスの返却
```

### フローチャート

```mermaid
flowchart TD
    A[リクエスト受信] --> B{エンドポイント}
    B -->|/widgets/cms/{id}| C[page - ウィジェット]
    B -->|/page/cms/{id}| D[pageFull - フルページ]
    B -->|/widgets/cms/navigation/{id}| E[category - カテゴリCMS]
    B -->|/widgets/cms/navigation/{id}/filter| F[filter - フィルター取得]
    B -->|/widgets/cms/buybox/{productId}/switch| G[switchBuyBoxVariant]

    C --> H{idパラメータあり?}
    H -->|No| I[RoutingException発生]
    H -->|Yes| J[CmsRoute::load]

    D --> J

    E --> K{navigationIdあり?}
    K -->|No| I
    K -->|Yes| L[CategoryRoute::load]
    L --> M{CMSページあり?}
    M -->|No| N[CmsException発生]
    M -->|Yes| O[CMSページ取得]

    J --> O
    O --> P[CmsPageLoadedHook実行]
    P --> Q[Twigレンダリング]
    Q --> R{ウィジェット?}
    R -->|Yes| S[x-robots-tag設定]
    R -->|No| T[ヘッダーなし]
    S --> U[レスポンス返却]
    T --> U
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | CMSページ必須 | 指定されたIDのCMSページが存在しない場合はエラー | 常時 |
| BR-002 | カテゴリCMSページ | カテゴリに紐付くCMSページがない場合はエラー | category()呼び出し時 |
| BR-003 | スロットフィルタリング | slotsパラメータ指定時は該当スロットのみ返却 | slots指定時 |
| BR-004 | ウィジェットnoindex | ウィジェット表示時は検索エンジンにインデックスさせない | page(), category()時 |

### 計算ロジック

特になし

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| CMSページ取得 | cms_page | SELECT | CMSページ本体の取得 |
| セクション取得 | cms_section | SELECT | ページ内セクションの取得 |
| ブロック取得 | cms_block | SELECT | セクション内ブロックの取得 |
| スロット取得 | cms_slot | SELECT | ブロック内スロットの取得 |
| カテゴリ取得 | category | SELECT | ナビゲーション経由時のカテゴリ取得 |

### テーブル別操作詳細

#### cms_page

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, name, type, config等 | id = {指定されたCMSページID} | - |

#### cms_section

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, position, type, config等 | cms_page_id = {CMSページID} | 位置順でソート |

#### cms_block

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, position, type, config等 | cms_section_id = {セクションID} | 位置順でソート |

#### cms_slot

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, slot, type, config等 | cms_block_id = {ブロックID} | - |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| FRAMEWORK__ROUTING_MISSING_REQUEST_PARAMETER | RoutingException | id/navigationIdパラメータ未指定 | パラメータを指定する |
| CMS__PAGE_NOT_FOUND | CmsException | 指定IDのCMSページが存在しない | 正しいIDを指定する |

### リトライ仕様

リトライ処理なし（読み取り専用処理のため）

## トランザクション仕様

読み取り専用のためトランザクション制御なし

## パフォーマンス要件

- HTTPキャッシュ対応（`PlatformRequest::ATTRIBUTE_HTTP_CACHE => true`）
- スロットフィルタリングによる部分的データ取得が可能
- レスポンス時間：200ms以内推奨

## セキュリティ考慮事項

- 認証不要（公開領域）
- ウィジェット表示時は`x-robots-tag: noindex`ヘッダーを設定しSEO重複を防止
- XSS対策：Twigのエスケープ機能を使用

## 備考

- CMSページはショッピング体験（Shopping Experiences）で管理
- セクション→ブロック→スロットの階層構造
- スロットには様々なタイプ（テキスト、画像、商品一覧等）が存在
- App Scriptsでの拡張はCmsPageLoadedHook経由で可能

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

CMSページの階層構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | CmsPageEntity.php | `src/Core/Content/Cms/CmsPageEntity.php` | CMSページのエンティティ構造 |
| 1-2 | CmsSectionEntity.php | `src/Core/Content/Cms/Aggregate/CmsSection/CmsSectionEntity.php` | セクションの構造 |
| 1-3 | CmsBlockEntity.php | `src/Core/Content/Cms/Aggregate/CmsBlock/CmsBlockEntity.php` | ブロックの構造 |
| 1-4 | CmsSlotEntity.php | `src/Core/Content/Cms/Aggregate/CmsSlot/CmsSlotEntity.php` | スロットの構造 |

**読解のコツ**: CMSページは「ページ → セクション → ブロック → スロット」の4階層構造。各スロットにはタイプ（text, image, product-listing等）があり、タイプごとに異なるデータ解決処理が行われる。

#### Step 2: エントリーポイントを理解する

処理の起点となるコントローラーを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | CmsController.php | `src/Storefront/Controller/CmsController.php` | 全メソッド（page, pageFull, category, filter, switchBuyBoxVariant） |

**主要処理フロー**:
1. **64-78行目**: page()メソッド - ウィジェットとしてCMSレイアウトを表示
2. **93-99行目**: pageFull()メソッド - フルページとしてCMSレイアウトを表示
3. **114-133行目**: category()メソッド - カテゴリに紐付くCMSページを表示
4. **147-169行目**: filter()メソッド - 商品一覧のフィルター情報を取得
5. **185-229行目**: switchBuyBoxVariant()メソッド - 商品バリアント切り替え

#### Step 3: Store APIルートを理解する

CMSページデータの取得処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | CmsRoute.php | `src/Core/Content/Cms/SalesChannel/CmsRoute.php` | CMSページ取得のStore APIルート |
| 3-2 | AbstractCmsRoute.php | `src/Core/Content/Cms/SalesChannel/AbstractCmsRoute.php` | 抽象クラスの定義 |

**主要処理フロー**:
- **41-67行目**: load()メソッド - CMSページローダーを使用してページを取得

#### Step 4: CMSページローダーを理解する

CMSページの解決処理（スロットデータの読み込み）を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | SalesChannelCmsPageLoader.php | `src/Core/Content/Cms/SalesChannel/SalesChannelCmsPageLoader.php` | CMSページの解決処理 |

### プログラム呼び出し階層図

```
CmsController
    │
    ├─ page() / pageFull()
    │      └─ AbstractCmsRoute::load()
    │             └─ SalesChannelCmsPageLoader::load()
    │                    └─ CmsSlotResolver (各スロットタイプの解決)
    │
    ├─ category()
    │      └─ AbstractCategoryRoute::load()
    │             └─ Category::getCmsPage()
    │
    ├─ filter()
    │      └─ AbstractProductListingRoute::load()
    │             └─ Aggregations取得
    │
    └─ switchBuyBoxVariant()
           ├─ AbstractFindProductVariantRoute::load()
           ├─ AbstractProductDetailRoute::load()
           └─ AbstractProductReviewLoader::load()
```

### データフロー図

```
[入力]                      [処理]                           [出力]

Request (id/navigationId) ────────┐
                                  ▼
SalesChannelContext ──────▶ CmsController ──────▶ CmsPageEntity
                                  │                     │
                                  ▼                     ├─ sections[]
                           CmsRoute/CategoryRoute       │    └─ blocks[]
                                  │                     │         └─ slots[]
                                  ▼                     │
                           SalesChannelCmsPageLoader    │
                                  │                     ▼
                                  ▼               Twig Template
                           CmsSlotResolver              │
                           (スロットデータ解決)          ▼
                                                  HTML Response
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| CmsController.php | `src/Storefront/Controller/CmsController.php` | コントローラー | CMSページ表示のエントリーポイント |
| CmsRoute.php | `src/Core/Content/Cms/SalesChannel/CmsRoute.php` | Store API | CMSページ取得ルート |
| AbstractCmsRoute.php | `src/Core/Content/Cms/SalesChannel/AbstractCmsRoute.php` | 抽象クラス | CMSルートの抽象定義 |
| CmsRouteResponse.php | `src/Core/Content/Cms/SalesChannel/CmsRouteResponse.php` | レスポンス | CMSルートレスポンス |
| SalesChannelCmsPageLoader.php | `src/Core/Content/Cms/SalesChannel/SalesChannelCmsPageLoader.php` | ローダー | CMSページの読み込みと解決 |
| CmsPageEntity.php | `src/Core/Content/Cms/CmsPageEntity.php` | エンティティ | CMSページのデータ構造 |
| CmsPageLoadedHook.php | `src/Storefront/Page/Cms/CmsPageLoadedHook.php` | フック | App Scripts用拡張ポイント |
