# 機能設計書 5-スニペット管理

## 概要

本ドキュメントは、Ghost CMSにおけるスニペット管理機能について、その設計仕様を記載する。スニペット管理機能は、再利用可能なコンテンツブロックの作成・管理を担う。

### 本機能の処理概要

スニペット管理機能は、記事やページで再利用できるコンテンツブロック（スニペット）を管理する機能である。スニペットはLexicalまたはMobiledoc形式で保存され、エディタから挿入することで同じコンテンツを複数の記事で共有できる。署名、免責事項、プロモーションバナーなど、頻繁に使用するコンテンツを効率的に管理できる。

**業務上の目的・背景**：コンテンツ作成において、同じ文章やブロックを複数の記事で使用したいケースがある。スニペット機能により、一度作成したコンテンツを再利用でき、更新が必要な場合も一箇所を修正するだけで済む。これにより、コンテンツの一貫性維持と作業効率の向上を実現する。

**機能の利用シーン**：
- ライターが記事末尾に著者紹介スニペットを挿入する
- 法務担当者が免責事項スニペットを作成し、関連記事に挿入する
- マーケティング担当者がプロモーションバナースニペットを作成する
- 編集者が署名ブロックをスニペットとして保存し、複数の記事で使用する

**主要な処理内容**：
1. スニペットの新規作成（POST /ghost/api/admin/snippets/）
2. スニペット一覧の取得（GET /ghost/api/admin/snippets/）
3. スニペットの編集・更新（PUT /ghost/api/admin/snippets/:id/）
4. スニペットの削除（DELETE /ghost/api/admin/snippets/:id/）

**関連システム・外部連携**：
- Lexicalエディタとの連携（スニペットの挿入UI）
- 記事管理機能との連携（スニペットの埋め込み）

**権限による制御**：
- Owner/Administrator/Editor/Author: スニペットの作成・編集・削除・閲覧が可能
- Contributor: スニペットの閲覧と使用のみ可能（権限設定による）

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 13 | エディタ画面 | 補助機能 | スニペットの挿入と保存 |

## 機能種別

CRUD操作

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | スニペット名 | 一意制約 |
| mobiledoc | string | No | Mobiledoc形式のコンテンツ（JSON） | 有効なMobiledoc JSON形式 |
| lexical | string | No | Lexical形式のコンテンツ（JSON） | 有効なLexical JSON形式 |

### 入力データソース

- 管理画面（Ghost Admin）のエディタからのスニペット保存
- Admin APIを通じた外部クライアントからのリクエスト

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | スニペットの一意識別子 |
| name | string | スニペット名 |
| mobiledoc | string | Mobiledoc形式のコンテンツ |
| lexical | string | Lexical形式のコンテンツ |
| created_at | datetime | 作成日時 |
| updated_at | datetime | 更新日時 |
| created_by | string | 作成者ID |
| updated_by | string | 更新者ID |

### 出力先

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

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ リクエストパラメータのバリデーション
2. 権限チェック
   └─ ユーザーロールの確認
3. 一意性チェック（作成時）
   └─ 同名のスニペットが存在しないか確認
4. URL変換処理
   └─ mobiledoc/lexical内のURLを相対形式に変換
5. データベース操作
   └─ snippetsテーブルへの挿入/更新/削除
6. レスポンス返却
   └─ スニペットデータをJSON形式で返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト受信] --> B{権限チェック}
    B -->|権限なし| C[403エラー]
    B -->|権限あり| D{操作種別}
    D -->|作成| E{同名スニペット存在?}
    E -->|Yes| F[400エラー]
    E -->|No| G[URL変換処理]
    D -->|更新| G
    D -->|削除| H[DB削除]
    G --> I[DB保存]
    I --> J[レスポンス返却]
    H --> J
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 名前一意制約 | スニペット名はシステム全体で一意である必要がある | スニペット作成時 |
| BR-002 | フォーマット選択 | mobiledocまたはlexicalのいずれかでコンテンツを保存 | 全スニペット操作時 |
| BR-003 | URL変換 | 保存時に絶対URLを相対形式に変換し、読み取り時に絶対形式に戻す | 全スニペット保存・取得時 |

### 計算ロジック

- **URL変換（保存時）**: mobiledoc/lexical内の絶対URLを`__GHOST_URL__`プレースホルダに変換
- **URL変換（読み取り時）**: `__GHOST_URL__`を実際のサイトURLに復元

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| スニペット作成 | snippets | INSERT | 新規スニペットレコードの挿入 |
| スニペット更新 | snippets | UPDATE | スニペットデータの更新 |
| スニペット削除 | snippets | DELETE | スニペットレコードの削除 |

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

#### snippets

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成 | 24文字の文字列 |
| INSERT | name | リクエスト値 | 必須、一意制約 |
| INSERT | mobiledoc | リクエスト値（URL変換後） | JSON形式 |
| INSERT | lexical | リクエスト値（URL変換後） | JSON形式 |
| INSERT | created_by | 現在のユーザーID | 自動設定 |
| UPDATE | updated_at | 現在日時 | 自動更新 |
| UPDATE | updated_by | 現在のユーザーID | 自動設定 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | ValidationError | 同名のスニペットが既に存在 | 別の名前を指定 |
| 403 | NoPermissionError | 権限不足 | 適切な権限を持つユーザーで実行 |
| 404 | NotFoundError | スニペットが存在しない | 正しいIDを指定 |

### リトライ仕様

特別なリトライ処理は実装されていない。一意制約違反の場合は明確なエラーメッセージを返す。

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

特別なトランザクション制御は実装されていない（単一テーブル操作のため）。

## パフォーマンス要件

- スニペット一覧取得: 100件以内であれば500ms以内
- スニペット作成・更新・削除: 500ms以内

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

- 認証: Ghost Admin SessionまたはAdmin API Key認証が必要
- 認可: 権限チェック（permissions: true）が設定されている
- XSS対策: スニペットコンテンツはLexical/Mobiledocとして保存され、レンダリング時にサニタイズ
- 監査ログ: スニペットの作成・編集・削除はactionsテーブルに記録（actionsCollectCRUD: true）

## 備考

- スニペットは記事内に埋め込まれるため、スニペットを更新しても既に公開された記事のコンテンツは自動更新されない（エディタで再挿入が必要）
- formatsオプションでmobiledocまたはlexicalの出力形式を指定可能

---

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

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

### 推奨読解順序

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

スニペットのモデル定義を確認する。

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

**主要処理フロー**:
- **7-11行目**: テーブル名とアクション収集設定（actionsCollectCRUD: true）
- **13-26行目**: formatOnWriteでURL変換処理（mobiledoc, lexical）
- **28-40行目**: parseでURL復元処理
- **42-54行目**: formatsToJSONでフォーマット制御
- **55-61行目**: toJSONでformatsToJSONを適用
- **63行目**: allowedFormats: ['mobiledoc', 'lexical']

**読解のコツ**: mobiledocLib.cardsとlexicalLib.nodesを使用してカード/ノード内のURLも変換している点に注目。

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

スニペットAPIのエンドポイント定義を確認する。

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

**主要処理フロー**:
- **14-36行目**: browseアクション（一覧取得）- formatsオプション対応
- **38-59行目**: readアクション（単一取得）
- **61-80行目**: addアクション（新規作成）- 一意制約違反のハンドリング
- **82-108行目**: editアクション（更新）
- **110-129行目**: destroyアクション（削除）

**重要ポイント**:
- addアクションで一意制約違反をキャッチし、専用のエラーメッセージを返す（72-78行目）
- すべてのアクションでpermissions: trueが設定されている
- formatsオプションでmobiledoc/lexicalの出力を制御可能

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

```
API Layer (snippets.js)
    │
    ├─ query(frame)
    │      │
    │      └─ Snippet Model (snippet.js)
    │             │
    │             ├─ findPage() [browse]
    │             │
    │             ├─ findOne() [read]
    │             │
    │             ├─ add() [add]
    │             │      ├─ formatOnWrite() - URL変換
    │             │      └─ 一意制約チェック
    │             │
    │             ├─ edit() [edit]
    │             │      └─ formatOnWrite() - URL変換
    │             │
    │             └─ destroy() [destroy]
    │
    └─ toJSON()
           └─ formatsToJSON() - フォーマット制御
```

### データフロー図

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

Admin UI / API Client
       │
       ▼
┌─────────────────┐
│ POST /snippets/ │
│ PUT /snippets/  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Validation      │───▶│ Permission      │
│ (name unique)   │    │ Check           │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Snippet Model   │
                       │ formatOnWrite() │
                       │ - mobiledoc URL │
                       │ - lexical URL   │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Database        │
                       │ snippets table  │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ toJSON()        │
                       │ parse() URL復元 │
                       │ formatsToJSON() │
                       └────────┬────────┘
                                │
                                ▼
                       JSON Response
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| snippet.js | `ghost/core/core/server/models/snippet.js` | モデル | Snippetモデル定義 |
| snippets.js | `ghost/core/core/server/api/endpoints/snippets.js` | API | Admin API エンドポイント |
| mobiledoc.js | `ghost/core/core/server/lib/mobiledoc.js` | ライブラリ | Mobiledoc処理 |
| lexical.js | `ghost/core/core/server/lib/lexical.js` | ライブラリ | Lexical処理 |
