# 機能設計書 1-記事管理

## 概要

本ドキュメントは、Ghost CMSにおける記事（Post）の管理機能について、その設計仕様を記載する。記事管理はGhostの中核機能であり、コンテンツの作成・編集・公開・削除に関わるすべての処理を担う。

### 本機能の処理概要

記事管理機能は、ブログ記事やニュースレターコンテンツの全ライフサイクルを管理する機能である。Lexicalエディタによるリッチテキスト編集、下書き・予約投稿・即時公開などの公開状態管理、タグや著者の関連付け、メール配信との連携など、包括的なコンテンツ管理を実現する。

**業務上の目的・背景**：GhostはパブリッシングプラットフォームとしてのCMSであり、記事管理は最も重要なビジネス要件である。コンテンツクリエイターが効率的に記事を作成・編集・公開できる環境を提供することで、質の高いコンテンツ配信を支援する。また、ニュースレター配信機能との統合により、会員向けのメールマガジン配信もシームレスに行える。

**機能の利用シーン**：
- ブロガーが新規記事を作成し、リッチテキストエディタで本文を執筆する
- 編集者が下書き記事をレビューし、公開日時を設定して予約投稿する
- 管理者が公開済み記事を更新し、即座に変更をサイトに反映する
- マーケティング担当者が記事をニュースレターとしてメンバーに配信する

**主要な処理内容**：
1. 記事の新規作成（POST /ghost/api/admin/posts/）
2. 記事一覧の取得とフィルタリング（GET /ghost/api/admin/posts/）
3. 記事の編集・更新（PUT /ghost/api/admin/posts/:id/）
4. 記事の削除（DELETE /ghost/api/admin/posts/:id/）
5. 記事の複製（POST /ghost/api/admin/posts/:id/copy/）
6. 一括操作（公開取消、特集指定など）（PUT /ghost/api/admin/posts/bulk/）
7. 公開状態の管理（draft/scheduled/published/sent）
8. Lexical/Mobiledocフォーマットの処理とHTML生成
9. リビジョン（編集履歴）の自動保存

**関連システム・外部連携**：
- ニュースレター配信サービス（email-service）との連携による記事のメール配信
- Stripeとの連携による有料コンテンツのアクセス制御
- 検索インデックスサービスへの自動インデックス更新

**権限による制御**：
- Owner/Administrator: すべての記事操作が可能
- Editor: 自分および他の著者の記事を編集・公開可能
- Author: 自分の記事のみ作成・編集可能、公開にはEditor以上の承認が必要
- Contributor: 下書きの作成のみ可能、公開不可、ステータス変更不可

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | ホーム画面 | 遷移先機能 | 最新記事一覧へのナビゲーション |
| 11 | 投稿一覧画面 | 主画面 | 記事の一覧表示・フィルタリング・検索 |
| 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/sent |
| 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 を含むオブジェクト |
| newsletter | string | No | 配信するニュースレターのslug | 有効なニュースレターslug |
| email_segment | string | No | メール配信対象セグメント | NQLフィルター形式 |
| 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 | 公開範囲 |
| created_at | datetime | 作成日時 |
| updated_at | datetime | 更新日時 |
| published_at | datetime | 公開日時 |
| tags | array | 関連タグ一覧 |
| authors | array | 著者一覧 |
| primary_tag | object | プライマリタグ |
| primary_author | object | プライマリ著者 |
| tiers | array | アクセス可能なTier一覧 |

### 出力先

- APIレスポンス（JSON形式）
- データベース（postsテーブルおよび関連テーブル）
- フロントエンドレンダリング（HTMLとして公開サイトに表示）
- ニュースレター（メール配信サービス経由）

## 処理フロー

### 処理シーケンス

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

### フローチャート

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

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 公開日時制約 | 予約投稿の公開日時は現在時刻から最低2分以上先の必要がある | status=scheduledかつpublished_at変更時 |
| BR-002 | 公開済み→予約不可 | 公開済み記事を予約投稿状態に変更することはできない | status=publishedからstatus=scheduledへの変更時 |
| BR-003 | Contributor制約 | Contributorロールのユーザーは下書き以外のステータスに変更できない | ユーザーロール=Contributor |
| BR-004 | スラッグ自動生成 | タイトルからスラッグを自動生成し、重複時はサフィックスを付与 | slug未指定かつtitle変更時 |
| BR-005 | email_only記事 | email_onlyフラグが設定された記事は公開時にstatus=sentとなる | posts_meta.email_only=true |
| BR-006 | Tier制約 | 無料Tierは記事に関連付けできない | tiers設定時 |

### 計算ロジック

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

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 記事作成 | posts | INSERT | 新規記事レコードの挿入 |
| 記事作成 | 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 | 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 | リクエスト値または現在日時 | 公開時に自動設定 |
| UPDATE | published_by | 公開実行ユーザーID | 公開時に自動設定 |

#### post_revisions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成 | |
| INSERT | post_id | 対象記事のID | |
| INSERT | lexical | 現在のLexical JSON | |
| INSERT | title | 現在のタイトル | |
| INSERT | author_id | 更新実行ユーザーID | |
| INSERT | created_at_ts | 現在のタイムスタンプ（ミリ秒） | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | ValidationError | Lexical/Mobiledocの形式が不正 | 正しい形式で再送信 |
| 400 | ValidationError | 予約投稿日時が過去または近すぎる | 2分以上先の日時を指定 |
| 403 | NoPermissionError | ユーザーに編集権限がない | 適切な権限を持つユーザーで実行 |
| 404 | NotFoundError | 指定されたIDの記事が存在しない | 正しいIDを指定 |
| 422 | MemberLimitError | メンバー数が制限を超過した状態で公開を試みた | メンバー数を削減するかプランをアップグレード |

### リトライ仕様

データベース操作はトランザクション内で実行されるため、エラー発生時は自動的にロールバックされる。クライアント側でのリトライは手動で行う必要がある。

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

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

## パフォーマンス要件

- 記事一覧取得: 100件以内であれば500ms以内
- 記事保存: 1000ms以内
- 大量の記事を含む一括操作: 10秒以内

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

- 認証: Ghost Admin SessionまたはAdmin API Key認証が必要
- 認可: ユーザーロールに基づく操作権限チェック
- XSS対策: Lexicalレンダリング時にサニタイズ処理
- CSRF対策: セッションベースの認証でCSRFトークン検証
- 監査ログ: 記事の作成・編集・削除はactionsテーブルに記録

## 備考

- MobiledocフォーマットはLexicalへの移行が進んでおり、新規記事はLexicalで作成される
- 既存のMobiledoc記事はconvert_to_lexicalオプションで変換可能
- リビジョンは最大25件まで保存され、古いものから自動削除される

---

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

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

### 推奨読解順序

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

まず、記事データの構造とスキーマを理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | postsテーブル（61-105行目）、posts_metaテーブル（106-122行目）、posts_tagsテーブル（297-305行目）のスキーマ定義を確認 |

**読解のコツ**: スキーマ定義では各カラムの型、最大長、NULL許容、デフォルト値、バリデーションルールが定義されている。`validations.isIn`で許容値の制限を確認できる。

#### Step 2: モデル層を理解する

データベース操作の中心となるPostモデルの実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | post.js | `ghost/core/core/server/models/post.js` | Postモデルの全体構造とBookshelfベースのORM実装 |

**主要処理フロー**:
- **45-47行目**: テーブル名とアクション収集の設定
- **72-100行目**: defaultsメソッドでデフォルト値を定義（status='draft'、visibility='public'など）
- **102-119行目**: リレーション定義（tags, authors, mobiledoc_revisions, post_revisions, posts_meta, tiers）
- **534-978行目**: onSavingフックで保存前処理（スラッグ生成、HTML生成、リビジョン保存）
- **1287-1325行目**: editメソッドでの更新処理
- **1340-1362行目**: addメソッドでの追加処理
- **1386-1448行目**: permissibleメソッドでの権限チェック

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

ビジネスロジックを実装するPostsServiceを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | posts-service.js | `ghost/core/core/server/services/posts/posts-service.js` | 記事のCRUD操作とビジネスロジック |

**主要処理フロー**:
- **35-38行目**: browsePostsメソッドで記事一覧取得
- **40-50行目**: readPostメソッドで単一記事取得
- **63-77行目**: editPostメソッドで記事更新（PostEmailHandlerと連携）
- **105-189行目**: bulkEditメソッドで一括編集（unpublish, unschedule, feature, unfeature, access, addTag）
- **324-330行目**: bulkDestroyメソッドで一括削除
- **450-513行目**: copyPostメソッドで記事複製

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

RESTful APIのエンドポイント定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | posts.js | `ghost/core/core/server/api/endpoints/posts.js` | Admin API エンドポイントの定義 |

**主要処理フロー**:
- **50-82行目**: browseアクション（一覧取得）
- **114-149行目**: readアクション（単一取得）
- **151-182行目**: addアクション（新規作成）
- **184-233行目**: editアクション（更新）
- **267-281行目**: bulkDestroyアクション（一括削除）
- **283-308行目**: destroyアクション（単一削除）
- **310-333行目**: copyアクション（複製）

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

```
API Layer (posts.js)
    │
    ├─ query(frame)
    │      │
    │      └─ PostsService (posts-service.js)
    │             │
    │             ├─ browsePosts()
    │             │      └─ Post.findPage()
    │             │
    │             ├─ readPost()
    │             │      └─ Post.findOne()
    │             │
    │             ├─ editPost()
    │             │      ├─ PostEmailHandler.validateBeforeSave()
    │             │      ├─ Post.edit()
    │             │      │      └─ onSaving() [モデルフック]
    │             │      │             ├─ スラッグ生成
    │             │      │             ├─ Lexical→HTML変換
    │             │      │             └─ リビジョン保存
    │             │      └─ PostEmailHandler.createOrRetryEmail()
    │             │
    │             ├─ bulkEdit()
    │             │      └─ #updatePosts() / #bulkAddTags()
    │             │
    │             └─ copyPost()
    │                    └─ Post.add()
    │
    └─ Model Layer (post.js)
           │
           └─ Bookshelf ORM
                  └─ Database (posts, posts_meta, posts_tags, ...)
```

### データフロー図

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

Admin UI / API Client
       │
       ▼
┌─────────────────┐
│ POST /posts/    │
│ PUT /posts/:id  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Validation      │───▶│ Permission      │
│ (入力検証)       │    │ Check (権限確認) │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ PostsService    │
                       │ (ビジネスロジック) │
                       └────────┬────────┘
                                │
         ┌──────────────────────┼──────────────────────┐
         ▼                      ▼                      ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│ Lexical/        │    │ Post Model      │    │ PostEmailHandler│
│ Mobiledoc Lib   │    │ (DB操作)         │    │ (メール連携)      │
│ (HTML生成)       │    └────────┬────────┘    └─────────────────┘
└─────────────────┘             │
                                ▼
                       ┌─────────────────┐
                       │ Database        │
                       │ posts, posts_*  │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Event Emission  │
                       │ post.published  │
                       │ post.edited     │
                       └────────┬────────┘
                                │
                                ▼
                       JSON Response / Cache Invalidation
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| post.js | `ghost/core/core/server/models/post.js` | モデル | Postモデル定義、DB操作、ビジネスロジック |
| posts-service.js | `ghost/core/core/server/services/posts/posts-service.js` | サービス | 記事管理のサービス層 |
| posts.js | `ghost/core/core/server/api/endpoints/posts.js` | API | Admin API エンドポイント |
| posts-public.js | `ghost/core/core/server/api/endpoints/posts-public.js` | API | Content API エンドポイント |
| post-revision.js | `ghost/core/core/server/models/post-revision.js` | モデル | リビジョンモデル定義 |
| post-scheduling-service.js | `ghost/core/core/server/services/posts/post-scheduling-service.js` | サービス | 予約投稿のスケジューリング |
| post-email-handler.js | `ghost/core/core/server/services/posts/post-email-handler.js` | サービス | メール配信連携 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | DBスキーマ定義 |
| lexical.js | `ghost/core/core/server/lib/lexical.js` | ライブラリ | Lexical JSON処理 |
| mobiledoc.js | `ghost/core/core/server/lib/mobiledoc.js` | ライブラリ | Mobiledoc JSON処理 |
