# 機能設計書 2-ページ管理

## 概要

本ドキュメントは、Ghost CMSにおける固定ページ（Page）の管理機能について、その設計仕様を記載する。ページ管理は記事管理と同じデータモデルを共有しつつ、固定コンテンツとしての特性を持つページの作成・編集・公開を担う。

### 本機能の処理概要

ページ管理機能は、「About」「Contact」「Privacy Policy」などの固定ページを管理する機能である。記事（Post）と同じLexicalエディタを使用し、同じデータベーステーブル（posts）を共有するが、type='page'として区別される。ページはニュースレター配信の対象とならず、タイムラインに表示されない静的なコンテンツとして扱われる。

**業務上の目的・背景**：Webサイトには記事コンテンツだけでなく、サイトの基本情報や法的文書などの固定ページが必要である。ページ管理機能により、これらの静的コンテンツを記事と同等のリッチテキスト編集環境で作成・管理できる。記事とは異なりタイムラインに表示されず、サイトナビゲーションからアクセスされることを想定している。

**機能の利用シーン**：
- サイト運営者が「About」ページを作成し、サイトの紹介文を記載する
- 法務担当者がプライバシーポリシーや利用規約ページを作成・更新する
- マーケティング担当者がランディングページを作成する
- デザイナーがカスタムテンプレートを適用した特別なページを作成する

**主要な処理内容**：
1. ページの新規作成（POST /ghost/api/admin/pages/）
2. ページ一覧の取得とフィルタリング（GET /ghost/api/admin/pages/）
3. ページの編集・更新（PUT /ghost/api/admin/pages/:id/）
4. ページの削除（DELETE /ghost/api/admin/pages/:id/）
5. ページの複製（POST /ghost/api/admin/pages/:id/copy/）
6. 一括操作（公開取消、特集指定など）（PUT /ghost/api/admin/pages/bulk/）
7. 公開状態の管理（draft/scheduled/published）

**関連システム・外部連携**：
- テーマテンプレートシステム（カスタムテンプレートの適用）
- 検索インデックスサービスへの自動インデックス更新

**権限による制御**：
- Owner/Administrator: すべてのページ操作が可能
- Editor: 自分および他の著者のページを編集・公開可能
- Author: 自分のページのみ作成・編集可能
- Contributor: 下書きの作成のみ可能、公開不可

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 12 | ページ一覧画面 | 主画面 | 固定ページの一覧表示・フィルタリング |
| 13 | エディタ画面 | 主画面 | ページの作成・編集・保存・公開（記事と共有） |

## 機能種別

CRUD操作 / コンテンツ変換処理 / イベント発行

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| title | string | No | ページタイトル | 最大255文字 |
| slug | string | No | URLスラッグ | 最大191文字、英数字とハイフン |
| lexical | string | No | Lexical形式のページ本文（JSON） | 有効なLexical JSON形式 |
| mobiledoc | string | No | Mobiledoc形式のページ本文（JSON） | 有効なMobiledoc JSON形式 |
| status | string | No | 公開状態 | draft/scheduled/published |
| featured | boolean | No | 特集ページフラグ | - |
| visibility | string | No | 公開範囲 | public/members/paid/tiers |
| published_at | datetime | No | 公開日時 | 予約投稿の場合は未来日時 |
| custom_excerpt | string | No | カスタム抜粋 | 最大300文字 |
| feature_image | string | No | アイキャッチ画像URL | 有効なURL |
| tags | array | No | タグの配列 | id または name を含むオブジェクト |
| authors | array | No | 著者の配列 | id を含むオブジェクト |
| custom_template | string | No | カスタムテンプレート名 | 有効なテンプレートファイル名 |
| tiers | array | No | アクセス可能なTierの配列 | id を含むオブジェクト |

### 入力データソース

- 管理画面（Ghost Admin）からのフォーム入力
- Admin APIを通じた外部クライアントからのリクエスト

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | ページの一意識別子（24文字） |
| uuid | string | UUID形式の識別子 |
| title | string | ページタイトル |
| slug | string | URLスラッグ |
| html | string | 生成されたHTML |
| plaintext | string | プレーンテキスト版 |
| feature_image | string | アイキャッチ画像URL |
| featured | boolean | 特集ページフラグ |
| status | string | 公開状態 |
| visibility | string | 公開範囲 |
| type | string | 常に'page' |
| created_at | datetime | 作成日時 |
| updated_at | datetime | 更新日時 |
| published_at | datetime | 公開日時 |
| tags | array | 関連タグ一覧 |
| authors | array | 著者一覧 |
| custom_template | string | 適用されているカスタムテンプレート |
| tiers | array | アクセス可能なTier一覧 |

### 出力先

- APIレスポンス（JSON形式）
- データベース（postsテーブルでtype='page'として保存）
- フロントエンドレンダリング（HTMLとして公開サイトに表示）

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ リクエストパラメータのバリデーション
   └─ type='page'の自動設定（入力シリアライザで追加）
2. 権限チェック
   └─ ユーザーロールとページ所有者の確認
3. 入力データの整形
   └─ タグの重複排除、スラッグの自動生成
4. Lexical/Mobiledocの処理
   └─ 本文からHTMLとプレーンテキストを生成
5. データベース操作
   └─ トランザクション内でpostsテーブル（type='page'）と関連テーブルを更新
6. リビジョンの保存
   └─ 変更内容をpost_revisionsテーブルに記録
7. イベント発行
   └─ page.added/page.edited/page.publishedなどのイベント発行
8. キャッシュ無効化
   └─ 公開状態変更時にCDNキャッシュを無効化
9. レスポンス返却
   └─ 更新されたページデータをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B[type='page'設定]
    B --> C{権限チェック}
    C -->|権限なし| D[403エラー]
    C -->|権限あり| E{ステータス変更?}
    E -->|No| F[ページデータ更新]
    E -->|Yes| G{公開への変更?}
    G -->|Yes| H[公開処理]
    G -->|No| I{下書きへの変更?}
    I -->|Yes| J[非公開処理]
    I -->|No| K[予約投稿処理]
    H --> F
    J --> F
    K --> F
    F --> L[Lexical→HTML変換]
    L --> M[DB保存]
    M --> N[リビジョン保存]
    N --> O[イベント発行]
    O --> P[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | type固定 | ページのtypeは常に'page'である | 全ページ操作時 |
| BR-002 | ニュースレター非対応 | ページはニュースレター配信の対象にならない | 常時 |
| BR-003 | 公開日時制約 | 予約投稿の公開日時は現在時刻から最低2分以上先の必要がある | status=scheduledかつpublished_at変更時 |
| BR-004 | Contributor制約 | Contributorロールのユーザーは下書き以外のステータスに変更できない | ユーザーロール=Contributor |
| BR-005 | スラッグ自動生成 | タイトルからスラッグを自動生成し、重複時はサフィックスを付与 | slug未指定かつtitle変更時 |
| BR-006 | スラッグ一意制約 | 同一typeの中でスラッグは一意である必要がある | slug設定時 |

### 計算ロジック

- **HTMLの生成**: Lexical JSONを解析し、各ノードタイプに応じたHTMLタグを生成する（記事と同じ処理）
- **プレーンテキストの生成**: 生成されたHTMLからタグを除去し、本文のテキストのみを抽出する
- **スラッグの生成**: タイトルを小文字化し、特殊文字を除去、スペースをハイフンに置換する

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ページ作成 | posts | INSERT | 新規ページレコードの挿入（type='page'） |
| ページ作成 | posts_meta | INSERT | メタデータレコードの挿入 |
| ページ作成 | posts_tags | INSERT | タグ関連の挿入 |
| ページ作成 | posts_authors | INSERT | 著者関連の挿入 |
| ページ更新 | posts | UPDATE | ページデータの更新 |
| ページ更新 | post_revisions | INSERT | リビジョンの追加 |
| ページ削除 | posts | DELETE | ページレコードの削除 |
| ページ削除 | posts_meta | DELETE | 関連メタデータの削除 |
| ページ削除 | posts_tags | DELETE | タグ関連の削除 |
| ページ削除 | posts_authors | DELETE | 著者関連の削除 |

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

#### posts

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | type | 'page'（固定値） | 記事との区別 |
| INSERT | id | crypto.randomUUID() で生成 | 24文字の文字列 |
| INSERT | uuid | crypto.randomUUID() で生成 | UUID形式 |
| INSERT | status | リクエスト値またはデフォルト'draft' | |
| UPDATE | html | Lexical/Mobiledocから生成 | 自動生成フィールド |
| UPDATE | plaintext | HTMLから抽出 | 自動生成フィールド |
| UPDATE | updated_at | 現在日時 | 自動更新 |
| UPDATE | published_at | リクエスト値または現在日時 | 公開時に自動設定 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | ValidationError | Lexical/Mobiledocの形式が不正 | 正しい形式で再送信 |
| 400 | ValidationError | 予約投稿日時が過去または近すぎる | 2分以上先の日時を指定 |
| 403 | NoPermissionError | ユーザーに編集権限がない | 適切な権限を持つユーザーで実行 |
| 404 | NotFoundError | 指定されたIDのページが存在しない | 正しいIDを指定 |

### リトライ仕様

データベース操作はトランザクション内で実行されるため、エラー発生時は自動的にロールバックされる。

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

ページの作成・更新・削除操作はすべてトランザクション内で実行される。postsテーブルと関連テーブル（posts_meta, posts_tags, posts_authors, post_revisions）への操作は一貫して成功するか、すべてロールバックされる。

## パフォーマンス要件

- ページ一覧取得: 100件以内であれば500ms以内
- ページ保存: 1000ms以内

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

- 認証: Ghost Admin SessionまたはAdmin API Key認証が必要
- 認可: ユーザーロールに基づく操作権限チェック（postsと同じ権限モデルを使用）
- XSS対策: Lexicalレンダリング時にサニタイズ処理
- 監査ログ: ページの作成・編集・削除はactionsテーブルに記録

## 備考

- ページはpostsテーブルでtype='page'として保存され、記事管理と同じPostモデルを使用する
- APIエンドポイントは /pages/ だが、権限チェックは docName: 'posts' として処理される
- ニュースレター関連のパラメータ（newsletter, email_segment）は使用されない

---

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

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

### 推奨読解順序

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

ページは記事と同じpostsテーブルを使用し、type='page'で区別される。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | postsテーブル（61-105行目）のスキーマ、特にtype列（73行目）のバリデーション |

**読解のコツ**: type列は`validations: {isIn: [['post', 'page']]}`で定義されており、'post'または'page'のみが許可される。

#### Step 2: APIエンドポイントを理解する

ページ専用のAPIエンドポイント定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | pages.js | `ghost/core/core/server/api/endpoints/pages.js` | ページ専用APIエンドポイント定義 |

**主要処理フロー**:
- **5行目**: ALLOWED_INCLUDESの定義（tags, authors, tiers, count.signups, count.paid_conversions, post_revisions）
- **17-49行目**: browseアクション（一覧取得）- 内部でmodels.Post.findPageを使用
- **51-94行目**: readアクション（単一取得）
- **96-128行目**: addアクション（新規作成）- frame.data.pages[0]からデータ取得
- **130-176行目**: editアクション（更新）- postsService.handleCacheInvalidationを使用
- **178-209行目**: bulkEditアクション（一括編集）
- **228-254行目**: destroyアクション（削除）
- **256-280行目**: copyアクション（複製）

**重要ポイント**:
- permissions設定で `docName: 'posts'` を指定しており、記事と同じ権限モデルを使用
- 実際のモデル操作はすべてmodels.Postを使用（ページ専用モデルは存在しない）

#### Step 3: 入力シリアライザでの type 設定を理解する

ページリクエストがどのようにtype='page'に変換されるかを確認する（入力シリアライザの処理）。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | post.js (モデル) | `ghost/core/core/server/models/post.js` | Postモデルでpage/postの両方を処理 |

**主要処理フロー**:
- **73行目**: type列のデフォルト値は'post'だが、ページAPIでは入力シリアライザでtype='page'に設定される
- **356-367行目**: emitChangeメソッドでリソースタイプに応じたイベント名を生成（'page.added'など）

#### Step 4: サービス層を理解する

ページもPostsServiceを使用する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | posts-service.js | `ghost/core/core/server/services/posts/posts-service.js` | 記事・ページ共通のサービス層 |

**主要処理フロー**:
- **422-448行目**: handleCacheInvalidationメソッド - ページの公開状態変更時のキャッシュ無効化処理
- **450-513行目**: copyPostメソッド - ページの複製にも使用される

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

```
API Layer (pages.js)
    │
    ├─ query(frame)
    │      │
    │      └─ models.Post.* (post.js モデル)
    │             │
    │             ├─ findPage() [browse]
    │             ├─ findOne() [read]
    │             ├─ add() [add] - type='page'がセット済み
    │             ├─ edit() [edit]
    │             └─ destroy() [destroy]
    │
    ├─ PostsService.handleCacheInvalidation() [edit]
    │
    └─ PostsService.copyPost() [copy]
           └─ models.Post.add()

※ permissions.docName = 'posts' により、記事と同じ権限チェック
```

### データフロー図

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

Admin UI / API Client
       │
       ▼
┌─────────────────┐
│ POST /pages/    │
│ PUT /pages/:id  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Input Serializer│
│ type='page'設定 │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Validation      │───▶│ Permission      │
│ (入力検証)       │    │ Check           │
└─────────────────┘    │ (docName:posts) │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Post Model      │
                       │ (type='page')   │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Database        │
                       │ posts table     │
                       │ WHERE type=page │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Event Emission  │
                       │ page.published  │
                       │ page.edited     │
                       └────────┬────────┘
                                │
                                ▼
                       JSON Response
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| pages.js | `ghost/core/core/server/api/endpoints/pages.js` | API | ページ専用Admin API エンドポイント |
| post.js | `ghost/core/core/server/models/post.js` | モデル | Post/Pageモデル定義（共用） |
| posts-service.js | `ghost/core/core/server/services/posts/posts-service.js` | サービス | 記事・ページ共通サービス層 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | DBスキーマ定義 |
| post-revision.js | `ghost/core/core/server/models/post-revision.js` | モデル | リビジョンモデル定義 |
