# 通知設計書 25-post.added

## 概要

本ドキュメントは、Ghost CMSにおける投稿追加時のWebhook通知（`post.added`イベント）の設計仕様を記載する。

### 本通知の処理概要

投稿（Post）が新規追加された際にトリガーされるWebhook通知である。外部システムとの連携（CMS連携、検索インデックス更新、バックアップシステム、分析ツールなど）を実現するための標準的なWebhook機能の一部として提供される。

**業務上の目的・背景**：現代のWebサイト運営では、CMSと外部サービスの連携が不可欠である。投稿が追加されたタイミングで外部システムに通知することで、検索エンジンへのインデックス登録、翻訳サービスへの連携、バックアップシステムへの通知、Slackやチャットツールへの転送など、様々な自動化ワークフローを構築できる。

**通知の送信タイミング**：投稿モデル（Post）で`onCreated`イベントが発火し、`post.added`イベントが発行された時点で、登録されているすべてのWebhookに対して順次HTTPリクエストが送信される。

**通知の受信者**：Ghost管理画面のIntegration設定でこのイベントに対して登録されたWebhookエンドポイント（外部URL）すべて。

**通知内容の概要**：追加された投稿の完全なJSONデータ（current）が含まれる。タグ、著者情報も関連データとして含まれる。

**期待されるアクション**：Webhook受信側システムは、投稿データを受け取り、必要な処理（インデックス更新、通知転送、データ同期など）を実行する。

## 通知種別

Webhook（HTTP POST）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（イベントリスナー経由） |
| 優先度 | 中 |
| リトライ | 5回（テスト環境では0回） |

### 送信先決定ロジック

1. `webhooks`テーブルから`event = 'post.added'`のレコードを全件取得
2. `customIntegrations`プラン制限が有効な場合、`internal`タイプのIntegrationに紐づくWebhookのみ対象
3. 各Webhookの`target_url`に対してHTTPリクエストを送信

## 通知テンプレート

### Webhook通知の場合

| 項目 | 内容 |
|-----|------|
| HTTPメソッド | POST |
| Content-Type | `application/json` |
| Content-Version | `v{ghostVersion.safe}` |
| タイムアウト | 2000ms |

### ペイロード構造

```json
{
    "post": {
        "current": {
            "id": "5ddc9141c35e7700383b2937",
            "uuid": "a5aa9bd8-ea31-415c-b452-3040dae1e730",
            "title": "Post Title",
            "slug": "post-title",
            "html": "<p>Post content...</p>",
            "plaintext": "Post content...",
            "feature_image": "https://example.com/image.jpg",
            "featured": false,
            "status": "draft",
            "visibility": "public",
            "created_at": "2024-01-01T00:00:00.000Z",
            "updated_at": "2024-01-01T00:00:00.000Z",
            "published_at": null,
            "tags": [...],
            "authors": [...]
        },
        "previous": {}
    }
}
```

### 署名ヘッダー（オプション）

Webhookに`secret`が設定されている場合：

```
X-Ghost-Signature: sha256={HMAC-SHA256(payload + timestamp)}, t={timestamp}
```

### 添付ファイル

なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| post.current | 現在の投稿データ | Postモデル（シリアライズ済み） | Yes |
| post.previous | 変更前データ（addedでは空） | 空オブジェクト | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| モデルイベント | `post.added` | Webhookが登録されている | 投稿作成時に自動送信 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| Webhook未登録 | このイベントに対するWebhookが1件も登録されていない場合 |
| インポート時 | `options.importing === true`の場合は送信しない |
| プラン制限 | `customIntegrations`制限時、internalタイプ以外のIntegrationのWebhookは除外 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Post.onCreated発火] --> B{importingフラグ?}
    B -->|true| C[終了]
    B -->|false| D[events.emit post.added]
    D --> E[WebhookTrigger.trigger]
    E --> F[getAll - Webhook取得]
    F --> G{customIntegrations制限?}
    G -->|あり| H[internalタイプのみフィルタ]
    G -->|なし| I[全Webhook]
    H --> I
    I --> J{Webhook数確認}
    J -->|0件| C
    J -->|1件以上| K[payloadシリアライズ]
    K --> L[各Webhookにループ]
    L --> M[HTTPリクエスト送信]
    M --> N{送信結果}
    N -->|成功| O[onSuccess - last_triggered更新]
    N -->|410| P[Webhook削除]
    N -->|その他エラー| Q[onError - エラー記録]
    O --> R{次のWebhook}
    P --> R
    Q --> R
    R -->|あり| M
    R -->|なし| C
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| webhooks | 登録Webhook取得 | `event = 'post.added'`でフィルタ |
| integrations | Integration情報 | プラン制限時の判定用 |
| posts | 投稿データ | イベント引数から取得 |

### テーブル別参照項目詳細

#### webhooks

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | Webhook識別 | `event = 'post.added'` |
| event | イベント種別 | フィルタ条件 |
| target_url | 送信先URL | 必須 |
| secret | 署名生成用シークレット | オプション |
| integration_id | Integration紐付け | プラン制限判定用 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| webhooks | UPDATE | 送信結果の記録 |
| webhooks | DELETE | 410レスポンス時の自動削除 |

#### webhooks更新詳細

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | last_triggered_at | 現在時刻 | 送信試行時 |
| UPDATE | last_triggered_status | HTTPステータスコード | 送信結果 |
| UPDATE | last_triggered_error | エラーメッセージ | エラー時のみ |
| DELETE | - | - | 410レスポンス時 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 410 Gone | Webhookエンドポイントが410を返した | Webhook自動削除 |
| タイムアウト | 2000ms以内にレスポンスなし | エラー記録、リトライ |
| ネットワークエラー | 接続失敗 | エラー記録、リトライ |
| その他HTTPエラー | 4xx/5xxレスポンス | エラー記録、リトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 5回（本番）、0回（テスト環境） |
| リトライ間隔 | @tryghost/requestライブラリのデフォルト |
| リトライ対象エラー | タイムアウト、ネットワークエラー、5xxエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | なし（イベント発生頻度に依存） |
| 1日あたり上限 | なし |

### 配信時間帯

制限なし（イベント発生時に即時送信）

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

- Webhook URLはIntegration管理者のみが設定可能
- `secret`設定時はHMAC-SHA256署名をヘッダーに付与し、受信側で検証可能
- `customIntegrations`プラン制限により、無料プランでは内部Integrationのみ使用可能
- Content-Versionヘッダーで送信元Ghostバージョンを明示

## 備考

- ペイロードのシリアライズはGhost APIと同じフォーマットを使用
- `previous`フィールドは`post.added`イベントでは常に空オブジェクト
- 複数のWebhookが登録されている場合、順次処理（並列ではない）
- Webhook削除（410レスポンス）はログに記録される

---

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

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

### 推奨読解順序

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

Webhookペイロードの構造とシリアライズ処理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | serialize.js | `ghost/core/core/server/services/webhooks/serialize.js` | ペイロード生成ロジック全体 |
| 1-2 | schema.js | `ghost/core/core/server/data/schema/schema.js` | 353-370行目、webhooksテーブル定義 |

**読解のコツ**: シリアライズ処理ではGhost APIのserializerを再利用しており、API出力と同じフォーマットでデータが生成される。

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

イベントリスナー登録と処理の起点を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | listen.js | `ghost/core/core/server/services/webhooks/listen.js` | イベント登録とトリガー |

**主要処理フロー**:
1. **10-45行目**: WEBHOOKSイベント一覧定義
2. **47-68行目**: `listen()`関数でイベントリスナー登録

#### Step 3: Webhook取得・送信ロジックを理解する

WebhookTriggerクラスの処理を追う。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | webhook-trigger.js | `ghost/core/core/server/services/webhooks/webhook-trigger.js` | WebhookTriggerクラス全体 |

**主要処理フロー**:
- **23-49行目**: `getAll()` - Webhook取得とプラン制限フィルタ
- **98-143行目**: `trigger()` - メイン送信処理
- **51-62行目**: `update()` - 送信結果記録
- **73-96行目**: `onSuccess()`/`onError()` - コールバック処理

#### Step 4: モデルイベント発火を理解する

Postモデルでのイベント発火を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | webhook.js | `ghost/core/core/server/models/webhook.js` | Webhookモデル定義 |

**主要処理フロー**:
- **46-53行目**: `findAllByEvent()` - イベント別Webhook取得

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

```
Ghost起動時
    │
    └─ listen() [listen.js:47-68]
           │
           └─ events.on('post.added', processWebhookTrigger)

Post作成時
    │
    └─ Post.onCreated → events.emit('post.added')
           │
           └─ processWebhookTrigger(model, options) [listen.js:59-66]
                  │
                  ├─ [importing?] return
                  │
                  └─ webhookTrigger.trigger('post.added', model) [webhook-trigger.js:98-143]
                         │
                         ├─ getAll('post.added') [23-49]
                         │      │
                         │      ├─ [customIntegrations制限?] internalフィルタ
                         │      │
                         │      └─ Webhook.findAllByEvent('post.added')
                         │
                         ├─ [各Webhookループ]
                         │      │
                         │      ├─ payload(event, model) [payload.js]
                         │      │      └─ serialize(event, model) [serialize.js]
                         │      │
                         │      ├─ 署名生成（secret設定時）
                         │      │
                         │      └─ request(url, opts)
                         │             │
                         │             ├─ [成功] onSuccess() → update()
                         │             │
                         │             ├─ [410] destroy()
                         │             │
                         │             └─ [エラー] onError() → update()
                         │
                         └─ [次のWebhook]
```

### データフロー図

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

Post.onCreated ───────▶ events.emit('post.added')
                              │
                              ▼
                        processWebhookTrigger()
                              │
                              ▼
webhooksテーブル ─────▶ getAll('post.added')
                              │
                              ▼
                        [各Webhookループ]
                              │
Post Model ───────────▶ payload() / serialize()
                              │
                              ▼
                        ペイロードJSON
                              │
webhook.secret ───────▶ 署名生成（オプション）
                              │
                              ▼
                        HTTPリクエスト構築
                              │
                              ▼
webhook.target_url ───▶ request() ─────────────────────▶ 外部Webhookエンドポイント
                              │
                        [レスポンス処理]
                              │
                              ▼
                        update() / destroy() ──────────▶ webhooksテーブル更新
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| listen.js | `ghost/core/core/server/services/webhooks/listen.js` | ソース | イベントリスナー登録 |
| webhook-trigger.js | `ghost/core/core/server/services/webhooks/webhook-trigger.js` | ソース | Webhook送信ロジック |
| payload.js | `ghost/core/core/server/services/webhooks/payload.js` | ソース | ペイロード生成エントリー |
| serialize.js | `ghost/core/core/server/services/webhooks/serialize.js` | ソース | ペイロードシリアライズ |
| webhook.js | `ghost/core/core/server/models/webhook.js` | ソース | Webhookモデル定義 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | webhooksテーブルスキーマ |
