# 機能設計書 3-タグ管理

## 概要

本ドキュメントは、Ghost CMSにおけるタグの管理機能について、その設計仕様を記載する。タグ管理機能は、記事を分類・整理するためのタグの作成・編集・削除を担う。

### 本機能の処理概要

タグ管理機能は、記事をカテゴリ分けするためのタグを管理する機能である。タグには名前、スラッグ、説明、アイキャッチ画像、SEOメタデータなどの属性を設定できる。また、内部タグ（#で始まる名前）を使用することで、公開サイトには表示されない管理用タグを作成することもできる。

**業務上の目的・背景**：コンテンツが増加すると、読者が目的の記事を見つけやすくするために分類・整理が必要になる。タグ機能により、記事をトピック別にグループ化し、関連コンテンツの発見を促進できる。また、タグページにSEO設定を行うことで、検索エンジンからのトラフィック獲得にも貢献する。

**機能の利用シーン**：
- 編集者が新しいトピック用のタグを作成する
- SEO担当者がタグページのメタタイトル・メタディスクリプションを設定する
- 管理者が内部タグを作成し、記事の管理状態を分類する
- デザイナーがタグのアイキャッチ画像を設定する

**主要な処理内容**：
1. タグの新規作成（POST /ghost/api/admin/tags/）
2. タグ一覧の取得とフィルタリング（GET /ghost/api/admin/tags/）
3. タグの編集・更新（PUT /ghost/api/admin/tags/:id/）
4. タグの削除（DELETE /ghost/api/admin/tags/:id/）
5. タグと記事の関連管理
6. 内部タグの自動検出と設定

**関連システム・外部連携**：
- 記事管理機能との連携（posts_tagsテーブル経由）
- テーマテンプレートシステム（タグページの表示）
- 検索インデックスサービスへの自動インデックス更新

**権限による制御**：
- Owner/Administrator/Editor: すべてのタグ操作が可能
- Author/Contributor: タグの閲覧のみ可能（新規作成・編集・削除は不可）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 11 | 投稿一覧画面 | 補助機能 | タグによるフィルタリング |
| 13 | エディタ画面 | 補助機能 | 記事へのタグ付与 |
| 15 | タグ一覧画面 | 主画面 | タグの一覧表示 |
| 16 | タグ編集画面 | 主画面 | タグの作成・編集・削除 |

## 機能種別

CRUD操作

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | タグ名 | 最大191文字、カンマ不可 |
| slug | string | No | URLスラッグ | 最大191文字、一意制約 |
| description | string | No | タグの説明 | 最大500文字 |
| feature_image | string | No | アイキャッチ画像URL | 最大2000文字、有効なURL |
| visibility | string | No | 表示設定 | public/internal |
| meta_title | string | No | SEOメタタイトル | 最大300文字 |
| meta_description | string | No | SEOメタディスクリプション | 最大500文字 |
| og_image | string | No | OGP画像URL | 最大2000文字 |
| og_title | string | No | OGPタイトル | 最大300文字 |
| og_description | string | No | OGP説明 | 最大500文字 |
| twitter_image | string | No | Twitter画像URL | 最大2000文字 |
| twitter_title | string | No | Twitterタイトル | 最大300文字 |
| twitter_description | string | No | Twitter説明 | 最大500文字 |
| codeinjection_head | string | No | ヘッダーコードインジェクション | 最大65535文字 |
| codeinjection_foot | string | No | フッターコードインジェクション | 最大65535文字 |
| canonical_url | string | No | カノニカルURL | 最大2000文字 |
| accent_color | string | No | アクセントカラー | 最大50文字（カラーコード） |

### 入力データソース

- 管理画面（Ghost Admin）からのフォーム入力
- Admin APIを通じた外部クライアントからのリクエスト
- 記事作成時のタグ自動作成（名前またはスラッグ指定）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | タグの一意識別子（24文字） |
| name | string | タグ名 |
| slug | string | URLスラッグ |
| description | string | タグの説明 |
| feature_image | string | アイキャッチ画像URL |
| visibility | string | 表示設定（public/internal） |
| meta_title | string | SEOメタタイトル |
| meta_description | string | SEOメタディスクリプション |
| og_image | string | OGP画像URL |
| og_title | string | OGPタイトル |
| og_description | string | OGP説明 |
| twitter_image | string | Twitter画像URL |
| twitter_title | string | Twitterタイトル |
| twitter_description | string | Twitter説明 |
| codeinjection_head | string | ヘッダーコードインジェクション |
| codeinjection_foot | string | フッターコードインジェクション |
| canonical_url | string | カノニカルURL |
| accent_color | string | アクセントカラー |
| created_at | datetime | 作成日時 |
| updated_at | datetime | 更新日時 |
| count.posts | number | 関連記事数（includeで取得時） |

### 出力先

- APIレスポンス（JSON形式）
- データベース（tagsテーブル）

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ リクエストパラメータのバリデーション
2. 権限チェック
   └─ ユーザーロールの確認
3. 内部タグ判定
   └─ 名前が#で始まる場合、visibility='internal'を自動設定
4. スラッグ生成
   └─ slug未指定時はnameからスラッグを生成
5. 既存タグの重複チェック
   └─ 同名（大文字小文字区別なし）のタグが存在する場合は既存タグを使用
6. データベース操作
   └─ tagsテーブルへの挿入/更新/削除
7. イベント発行
   └─ tag.added/tag.edited/tag.deletedイベント発行
8. キャッシュ無効化
   └─ 変更時にCDNキャッシュを無効化
9. レスポンス返却
   └─ タグデータをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B{権限チェック}
    B -->|権限なし| C[403エラー]
    B -->|権限あり| D{操作種別}
    D -->|作成| E{名前が#で始まる?}
    E -->|Yes| F[visibility=internal設定]
    E -->|No| G[visibility=public維持]
    F --> H[スラッグ生成]
    G --> H
    H --> I[DB保存]
    D -->|更新| J[既存タグ取得]
    J --> K{タグ存在?}
    K -->|No| L[404エラー]
    K -->|Yes| I
    D -->|削除| M[関連記事との紐付け解除]
    M --> N[DB削除]
    I --> O[イベント発行]
    N --> O
    O --> P[キャッシュ無効化]
    P --> Q[レスポンス返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 内部タグ自動設定 | タグ名が#で始まる場合、visibility='internal'が自動設定される | タグ作成時 |
| BR-002 | スラッグ一意制約 | スラッグはタグ間で一意である必要がある | 全タグ操作時 |
| BR-003 | スラッグ自動生成 | slug未指定時はnameからスラッグを自動生成 | タグ作成・更新時 |
| BR-004 | 大文字小文字区別なし | タグ名の重複チェックは大文字小文字を区別しない | タグ作成時 |
| BR-005 | カンマ禁止 | タグ名にカンマ(,)を含めることはできない | 全タグ作成・更新時 |
| BR-006 | 削除時の紐付け解除 | タグ削除時は関連する記事との紐付けを自動解除 | タグ削除時 |

### 計算ロジック

- **スラッグの生成**: タグ名を小文字化し、特殊文字を除去、スペースをハイフンに置換する
- **記事数カウント**: posts_tagsテーブルを結合し、各タグに紐付く公開済み記事数をカウントする

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| タグ作成 | tags | INSERT | 新規タグレコードの挿入 |
| タグ更新 | tags | UPDATE | タグデータの更新 |
| タグ削除 | posts_tags | DELETE | 関連記事との紐付け解除 |
| タグ削除 | tags | DELETE | タグレコードの削除 |

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

#### tags

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成（24文字） | |
| INSERT | name | リクエスト値 | 必須 |
| INSERT | slug | リクエスト値またはnameから生成 | 一意制約 |
| INSERT | visibility | 'public'または'internal' | #で始まる場合はinternal |
| UPDATE | updated_at | 現在日時 | 自動更新 |

#### posts_tags

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| DELETE | tag_id | 削除対象タグのID | タグ削除時に実行 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | ValidationError | タグ名が空またはカンマを含む | 有効なタグ名を指定 |
| 400 | ValidationError | スラッグが一意でない | 別のスラッグを指定 |
| 403 | NoPermissionError | Author/Contributorがタグを作成・編集・削除 | 適切な権限を持つユーザーで実行 |
| 404 | NotFoundError | 指定されたIDのタグが存在しない | 正しいIDを指定 |

### リトライ仕様

特別なリトライ処理は実装されていない。エラー発生時はクライアント側で対応する。

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

タグの削除操作では、posts_tagsテーブルからの関連レコード削除とtagsテーブルからのタグ削除が順次実行される（ghostBookshelf.Model.destroyの実装による）。

## パフォーマンス要件

- タグ一覧取得: 100件以内であれば500ms以内
- タグ作成・更新・削除: 500ms以内

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

- 認証: Ghost Admin SessionまたはAdmin API Key認証が必要
- 認可: Editor以上のロールでのみ作成・編集・削除可能
- XSS対策: codeinjection_head/footはそのまま出力されるため、管理者権限でのみ設定可能
- 監査ログ: タグの作成・編集・削除はactionsテーブルに記録（actionsCollectCRUD: true）

## 備考

- 内部タグ（#で始まる）はフロントエンドのタグ一覧には表示されないが、フィルタリングや管理目的で使用できる
- Content APIでは公開タグ（visibility='public'）のみが返される

---

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

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

### 推奨読解順序

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

タグのスキーマ定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | tagsテーブル（268-296行目）のスキーマ定義 |

**読解のコツ**:
- name列は `validations: {matches: /^([^,]|$)/}` でカンマを禁止している
- visibility列は `validations: {isIn: [['public', 'internal']]}` で2値のみ許可
- slugは `unique: true` で一意制約

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

Tagモデルの実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | tag.js | `ghost/core/core/server/models/tag.js` | Tagモデルの全体構造 |

**主要処理フロー**:
- **13-15行目**: テーブル名とアクション収集設定
- **17-24行目**: actionsCollectCRUD: trueで監査ログ記録
- **20-23行目**: デフォルト値（visibility='public'）
- **78-81行目**: emitChangeメソッドでイベント発行（tag.added/edited/deleted）
- **101-127行目**: onSavingフックで保存前処理
  - **110-112行目**: nameのみでslugなしの場合、nameをnameにセット
  - **115-117行目**: #で始まる場合、visibility='internal'を自動設定
  - **119-126行目**: スラッグの生成・重複チェック
- **169-187行目**: countRelationsで記事数カウントのサブクエリ定義
- **189-208行目**: destroyメソッドでposts_tagsの紐付け解除後にタグ削除

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

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

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

**主要処理フロー**:
- **5行目**: ALLOWED_INCLUDES定義（count.postsのみ）
- **15-39行目**: browseアクション（一覧取得）
- **41-74行目**: readアクション（単一取得）- id, slug, visibilityで検索可能
- **76-95行目**: addアクション（新規作成）- 201ステータス、キャッシュ無効化
- **97-130行目**: editアクション（更新）- 変更時のみキャッシュ無効化
- **132-154行目**: destroyアクション（削除）- 204ステータス、キャッシュ無効化

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

```
API Layer (tags.js)
    │
    ├─ query(frame)
    │      │
    │      └─ Tag Model (tag.js)
    │             │
    │             ├─ findPage() [browse]
    │             │      └─ countRelations.posts() サブクエリ
    │             │
    │             ├─ findOne() [read]
    │             │
    │             ├─ add() [add]
    │             │      └─ onSaving()
    │             │             ├─ 内部タグ判定
    │             │             └─ スラッグ生成
    │             │
    │             ├─ edit() [edit]
    │             │      └─ onSaving()
    │             │
    │             └─ destroy() [destroy]
    │                    ├─ posts.detach() - 紐付け解除
    │                    └─ tag.destroy()
    │
    └─ Event Emission
           ├─ tag.added
           ├─ tag.edited
           └─ tag.deleted
```

### データフロー図

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

Admin UI / API Client
       │
       ▼
┌─────────────────┐
│ POST /tags/     │
│ PUT /tags/:id   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Validation      │───▶│ Permission      │
│ (name, slug)    │    │ Check           │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Tag Model       │
                       │ onSaving()      │
                       │ - #判定         │
                       │ - スラッグ生成    │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Database        │
                       │ tags table      │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Event Emission  │
                       │ tag.added       │
                       │ tag.edited      │
                       └────────┬────────┘
                                │
                                ▼
                       JSON Response / Cache Invalidation
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| tag.js | `ghost/core/core/server/models/tag.js` | モデル | Tagモデル定義 |
| tags.js | `ghost/core/core/server/api/endpoints/tags.js` | API | Admin API エンドポイント |
| tags-public.js | `ghost/core/core/server/api/endpoints/tags-public.js` | API | Content API エンドポイント |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | スキーマ | DBスキーマ定義（tagsテーブル） |
