# 通知設計書 45-tag.deleted

## 概要

本ドキュメントは、Ghost CMSにおける `tag.deleted` Webhookイベントの設計仕様を定義する。このWebhookは、タグが削除された際に、登録されたWebhookエンドポイントに通知を送信する機能を提供する。

### 本通知の処理概要

このWebhookは、Ghost CMSでタグが削除された際に、外部システムへリアルタイムで通知を送信する機能を提供する。

**業務上の目的・背景**：タグの削除は、コンテンツの分類体系に影響を与える重要な操作である。タグの削除を外部システム（検索エンジン、コンテンツ管理システム、分析プラットフォーム、タクソノミー管理ツールなど）に通知することで、不要なタグ参照の除去、検索インデックスの更新、関連するURLのリダイレクト設定などの対応が可能になる。また、削除されたタグに紐づいていた投稿・ページの再分類が必要かどうかの判断材料にもなる。

**通知の送信タイミング**：Tagモデルの `onDestroyed` フックが呼び出された際に `tag.deleted` イベントが発火される。これはタグの削除（Admin APIまたは管理画面経由）が完了したタイミングで発生する。なお、タグ削除前に関連する投稿からタグの紐付けが解除される（posts_tagsテーブルからレコード削除）。

**通知の受信者**：Ghost管理画面で設定された `tag.deleted` イベントに対応するWebhookエンドポイント。これらはIntegration（連携機能）を通じて登録される外部システムのURLである。

**通知内容の概要**：削除されたタグの最終状態を含むJSONペイロード。タグの基本情報（ID、名前、スラッグなど）が `previous` に格納され、`current` は空オブジェクトとなる。

**期待されるアクション**：受信側システムは、タグ削除の通知を受け取り、タグマスターからの削除、検索インデックスからの除去、タグアーカイブページへのリダイレクト設定、関連するコンテンツの再インデックスなどの処理を実行することが期待される。

## 通知種別

Webhook（HTTP POST リクエスト）

## 送信仕様

### 基本情報

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

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

1. `webhooks` テーブルから `event = 'tag.deleted'` に一致するレコードを取得
2. カスタムインテグレーション制限（`customIntegrations` limit）が有効な場合は、内部インテグレーション（`type = 'internal'`）に紐づくWebhookのみを対象とする
3. 各Webhookの `target_url` に対してHTTP POSTリクエストを送信

## 通知テンプレート

### Webhook通知の場合

| 項目 | 内容 |
|-----|------|
| HTTPメソッド | POST |
| Content-Type | application/json |
| Content-Version | v{Ghost APIバージョン} |
| タイムアウト | 2000ms（2秒） |

### ペイロード構造

```json
{
  "tag": {
    "current": {},
    "previous": {
      "id": "削除されたタグID",
      "name": "削除されたタグ名",
      "slug": "削除されたタグスラッグ",
      "description": "タグの説明",
      "feature_image": "特集画像URL",
      "visibility": "public",
      "og_image": "OG画像URL",
      "og_title": "OGタイトル",
      "og_description": "OG説明",
      "twitter_image": "Twitter画像URL",
      "twitter_title": "Twitterタイトル",
      "twitter_description": "Twitter説明",
      "meta_title": "メタタイトル",
      "meta_description": "メタ説明",
      "accent_color": "アクセントカラー",
      "created_at": "作成日時",
      "updated_at": "更新日時"
    }
  }
}
```

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

Webhookに `secret` が設定されている場合、以下のヘッダーが追加される：

| ヘッダー名 | 形式 |
|----------|------|
| X-Ghost-Signature | `sha256={HMAC署名}, t={タイムスタンプ}` |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| tag.current | 現在の状態（削除済みのため空オブジェクト） | - | Yes |
| tag.previous.id | 削除されたタグの一意識別子 | tags.id | Yes |
| tag.previous.name | 削除されたタグ名 | tags.name | Yes |
| tag.previous.slug | 削除されたタグスラッグ | tags.slug | Yes |
| tag.previous.description | タグの説明文 | tags.description | No |
| tag.previous.feature_image | 特集画像URL | tags.feature_image | No |
| tag.previous.visibility | 可視性（public/internal） | tags.visibility | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | 管理画面でタグを削除 | Tag.onDestroyed発火 | 設定 > タグ から削除 |
| API | Admin API経由でタグ削除 | Tag.onDestroyed発火 | DELETE /admin/tags/:id |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| options.importing === true | インポート処理中は送信しない |
| 対象Webhookが存在しない | tag.deletedイベントに登録されたWebhookがない場合 |
| customIntegrations制限超過 | カスタムインテグレーション数制限を超えている場合は内部インテグレーションのみ |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[タグ削除操作] --> B[関連投稿からタグ紐付け解除]
    B --> C[Tag.destroy実行]
    C --> D[Tag.onDestroyed発火]
    D --> E[emitChange'deleted'呼び出し]
    E --> F[tag.deletedイベント発火]
    F --> G[WebhookTrigger.trigger呼び出し]
    G --> H[Webhook.findAllByEvent実行]
    H --> I{対象Webhook存在?}
    I -->|No| Z[処理終了]
    I -->|Yes| J[ペイロード生成]
    J --> K[各Webhookに対してHTTP POST]
    K --> L{送信成功?}
    L -->|Yes| M[last_triggered_at更新]
    L -->|No| N{ステータス410?}
    N -->|Yes| O[Webhook削除]
    N -->|No| P[last_triggered_error記録]
    M --> Z
    O --> Z
    P --> Z
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| webhooks | 対象Webhookの取得 | event='tag.deleted' |
| integrations | Webhook連携先の確認 | 内部インテグレーション判定用 |
| tags | 削除されるタグのデータ | previous値として使用 |
| posts_tags | 関連投稿の紐付け解除 | 削除前に処理される |

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

#### webhooks

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | Webhook識別 | - |
| event | イベント種別 | event = 'tag.deleted' |
| target_url | 送信先URL | - |
| secret | 署名生成用シークレット | NULLでない場合のみ使用 |
| integration_id | 連携先の識別 | インテグレーション制限判定用 |

#### tags

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id, name, slug | タグ基本情報（削除前） | 削除対象タグ |
| description | タグ説明 | - |
| feature_image | 特集画像 | - |
| visibility | 可視性 | - |
| og_*, twitter_*, meta_* | SEO関連情報 | - |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| posts_tags | DELETE | タグと投稿の紐付け解除（削除前） |
| tags | DELETE | タグの削除 |
| webhooks | UPDATE | 送信結果の記録 |

#### 削除処理の順序

1. `posts_tags` から該当タグの紐付けを全て削除（tag.js 202-203行目）
2. `tags` から該当タグを削除（tag.js 205行目）
3. `onDestroyed` フックでイベント発火

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| タイムアウト | 2秒以内に応答なし | リトライ実行 |
| 410 Gone | 受信側がWebhook削除を要求 | Webhookレコードを削除 |
| 接続エラー | ネットワーク障害 | リトライ実行、エラーログ記録 |
| 5xx エラー | サーバー側エラー | リトライ実行、エラーログ記録 |
| タグ未発見 | 存在しないタグIDの削除 | NotFoundErrorを返却 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 5回（本番環境）、0回（テスト環境） |
| リトライ間隔 | @tryghost/requestのデフォルト設定に従う |
| リトライ対象エラー | タイムアウト、接続エラー、5xxエラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし（イベント発生ごとに送信） |
| 1日あたり上限 | 制限なし |

### 配信時間帯

送信可能な時間帯の制限なし。イベント発生時に即座に送信される。

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

- **署名検証**: Webhookに `secret` が設定されている場合、`X-Ghost-Signature` ヘッダーでHMAC-SHA256署名を提供
- **HTTPS推奨**: `target_url` にはHTTPS URLの使用を推奨
- **タイムスタンプ**: 署名にタイムスタンプを含めることでリプレイ攻撃を防止

## 備考

- タグ削除時、関連する投稿からのタグ紐付けは自動的に解除される（tag.js destroy静的メソッド 189-208行目）
- 削除イベントでは `current` は空オブジェクト、`previous` に削除前のデータが格納される
- 削除対象タグが存在しない場合、NotFoundErrorが返却されWebhookは発火しない

---

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

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

### 推奨読解順序

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

Webhookとタグの基本構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | webhooksテーブル（353-370行目）、tagsテーブル（268-296行目）、posts_tagsテーブル（297-305行目）の定義を確認 |
| 1-2 | tag.js | `ghost/core/core/server/models/tag.js` | Tagモデルの構造、destroy静的メソッド、onDestroyedメソッド |

**読解のコツ**: destroyメソッドで投稿との紐付け解除が先に行われることを理解する。

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

イベント発火の起点を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | tag.js | `ghost/core/core/server/models/tag.js` | `destroy`静的メソッド（189-208行目）と`onDestroyed`メソッド（95-99行目） |

**主要処理フロー**:
1. **189行目**: `destroy`静的メソッドの開始
2. **193行目**: `options.withRelated = ['posts']` で関連投稿を取得
3. **195-196行目**: タグをフェッチ、存在しない場合はNotFoundError
4. **202-203行目**: `tag.related('posts').detach(null, options)` で投稿との紐付けを解除
5. **205行目**: `tag.destroy(options)` でタグ本体を削除
6. **95-99行目**: `onDestroyed`で `emitChange('deleted', options)` を呼び出し

#### Step 3: イベントリスナーの登録を理解する

Webhookシステムがどのようにイベントを購読するか。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | listen.js | `ghost/core/core/server/services/webhooks/listen.js` | WEBHOOKSリスト（10-45行目）と`listen`関数（47-68行目） |

**主要処理フロー**:
- **35行目**: `tag.deleted` がWEBHOOKSリストに定義
- **59-66行目**: `events.on`でイベントリスナーを登録、`webhookTrigger.trigger`を呼び出し

#### Step 4: Webhook送信処理とペイロード生成を理解する

削除時のペイロード構造（currentが空、previousに削除前データ）。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | webhook-trigger.js | `ghost/core/core/server/services/webhooks/webhook-trigger.js` | `trigger`メソッド（98-143行目） |
| 4-2 | serialize.js | `ghost/core/core/server/services/webhooks/serialize.js` | 削除時のペイロード生成（15-41行目、43-64行目） |

**主要処理フロー**:
- **serialize.js 15行目**: `model.attributes` が空の場合の処理
- **serialize.js 37-40行目**: attributesが空の場合は空オブジェクトを返す
- **serialize.js 43行目**: `model._previousAttributes` から削除前の値を取得

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

```
Tag.destroy (models/tag.js:189) [static method]
    │
    ├─ tag.related('posts').detach() (models/tag.js:202)
    │
    └─ tag.destroy() (models/tag.js:205) [instance method]
           │
           └─ Tag.onDestroyed (models/tag.js:95)
                  │
                  └─ emitChange('deleted') (models/tag.js:98)
                         │
                         └─ events.emit('tag.deleted') (lib/common/events.js)
                                │
                                └─ processWebhookTrigger (services/webhooks/listen.js:59)
                                       │
                                       └─ WebhookTrigger.trigger (services/webhooks/webhook-trigger.js:98)
                                              │
                                              ├─ getAll('tag.deleted') (webhook-trigger.js:23)
                                              │
                                              ├─ payload() (services/webhooks/payload.js)
                                              │      └─ serialize()
                                              │             ├─ current: {} (empty)
                                              │             └─ previous: 削除前データ
                                              │
                                              └─ request(url, opts) (HTTP送信)
```

### データフロー図

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

タグ削除要求             Tag.destroy
(API/管理画面)  ────▶    posts_tags削除  ────▶       紐付け解除
                              │
                              ▼
                        tags削除 ────▶ onDestroyed ────▶ イベント発火
                                                              │
                                                              ▼
                                                        WebhookTrigger
                                                              │
                                                              ▼
                        serialize()                    HTTP POST
                        ・current: {}                  (target_url)
                        ・previous: 削除前データ            │
                              │                            ▼
                              ▼                       外部システム
                        {tag: {
                          current: {},
                          previous: {id, name, slug, ...}
                        }}
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| 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` | ソース | リソースシリアライズ |
| tag.js | `ghost/core/core/server/models/tag.js` | ソース | Tagモデル・削除処理・イベント発火 |
| webhook.js | `ghost/core/core/server/models/webhook.js` | ソース | Webhookモデル定義 |
| events.js | `ghost/core/core/server/lib/common/events.js` | ソース | イベントエミッター |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
