# 機能設計書 60-サイトレコメンデーション

## 概要

本ドキュメントは、Ghostのサイトレコメンデーション機能に関する設計を記述します。この機能は、パブリッシャーが読者に他のサイトを推薦し、またレコメンデーションを受け取ることで、サイト間のネットワークを構築するための機能です。

### 本機能の処理概要

**業務上の目的・背景**：インディーウェブの精神に基づき、パブリッシャー同士が互いのサイトを推薦し合うことで、読者に価値のあるコンテンツを紹介し、ニュースレターコミュニティを拡大します。ワンクリック購読機能により、読者は簡単に推薦サイトを購読できます。

**機能の利用シーン**：
- 他のGhostサイトやニュースレターを推薦として追加
- 読者がレコメンデーションをクリック・購読
- 他サイトからのレコメンデーション通知を受信

**主要な処理内容**：
1. レコメンデーションのCRUD操作
2. ワンクリック購読の検証とメタデータ取得
3. Well-known URLへのレコメンデーション公開
4. クリック・購読イベントのトラッキング
5. 受信レコメンデーションの通知メール送信

**関連システム・外部連携**：
- Webmention: レコメンデーション通知の送受信
- oEmbed: サイトメタデータの取得

**権限による制御**：レコメンデーション管理は `recommendations` ドキュメントの権限に従います。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 34 | レコメンデーション設定画面 | 主画面 | レコメンデーション管理 |
| 35 | 受信レコメンデーション画面 | 参照画面 | 受信レコメンデーション一覧 |

## 機能種別

CRUD / 外部連携 / トラッキング / 通知

## 入力仕様

### 入力パラメータ

#### レコメンデーション追加

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| title | string | Yes | サイトタイトル | 必須 |
| url | URL | Yes | サイトURL | 有効なURL |
| one_click_subscribe | boolean | No | ワンクリック購読対応 | デフォルトfalse |
| description | string | No | 説明文 | 任意 |
| excerpt | string | No | 抜粋 | 任意 |
| featured_image | URL | No | アイキャッチ画像 | 有効なURL |
| favicon | URL | No | ファビコン | 有効なURL |

#### 一覧取得

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| page | number | No | ページ番号 | デフォルト1 |
| limit | number | No | 取得件数 | デフォルト5 |
| filter | string | No | NQLフィルター | 有効なNQL |
| order | string | No | ソート順 | 複数フィールド対応 |
| withRelated | array | No | 関連データ | count.clicks, count.subscribers |

### 入力データソース

- Admin API: レコメンデーション操作
- Members API: クリック・購読トラッキング
- Webmention: 受信レコメンデーション
- データベース: `recommendations`, `recommendation_click_events`, `recommendation_subscribe_events`

## 出力仕様

### 出力データ

#### レコメンデーションレスポンス

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | レコメンデーションID |
| title | string | サイトタイトル |
| url | URL | サイトURL |
| description | string | 説明文 |
| excerpt | string | 抜粋 |
| featured_image | URL | アイキャッチ画像 |
| favicon | URL | ファビコン |
| one_click_subscribe | boolean | ワンクリック購読対応 |
| created_at | string | 作成日時 |
| updated_at | string | 更新日時 |
| count.clicks | number | クリック数 |
| count.subscribers | number | 購読者数 |

### 出力先

- データベース: `recommendations`, `recommendation_click_events`, `recommendation_subscribe_events`
- Well-known: `/.well-known/recommendations.json`
- Webmention: 推薦先サイトへ通知

## 処理フロー

### 処理シーケンス

```
1. レコメンデーション追加
   └─ Recommendation.create()
   └─ 既存URL重複チェック
   └─ repository.save()
   └─ updateWellknown()
   └─ updateRecommendationsEnabledSetting()
   └─ sendMentionToRecommendation()

2. メタデータ取得
   └─ recommendationMetadataService.fetch()
   └─ oEmbed/外部リクエストでタイトル等取得
   └─ ワンクリック購読対応チェック

3. Well-known更新
   └─ wellknownService.set()
   └─ /.well-known/recommendations.json生成

4. トラッキング
   └─ trackClicked() / trackSubscribed()
   └─ イベント保存

5. 受信レコメンデーション
   └─ MentionCreatedEvent購読
   └─ recommendations.jsonパス判定
   └─ メール通知送信
```

### フローチャート

```mermaid
flowchart TD
    A[レコメンデーション追加] --> B{URL重複?}
    B -->|Yes| C[ValidationError]
    B -->|No| D[Recommendation.create]
    D --> E[repository.save]
    E --> F[updateWellknown]
    F --> G[updateEnabledSetting]
    G --> H[sendMentionToRecommendation]

    I[メタデータ取得] --> J[oEmbed/fetch]
    J --> K[oneClickSubscribeチェック]
    K --> L[edit保存]

    M[MentionCreatedEvent] --> N{recommendations.json?}
    N -->|Yes| O[sendRecommendationEmail]
    N -->|No| P[通常メンション処理]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-60-001 | URL一意制約 | 同一URLのレコメンデーションは1つのみ | 追加時 |
| BR-60-002 | 設定自動更新 | レコメンデーション数が0になると設定を無効化 | 削除時 |
| BR-60-003 | メタデータ定期更新 | 起動2-7分後にメタデータを一括更新 | 初期化時 |
| BR-60-004 | 削除時Webmention | 削除時もWebmentionを送信（仕様準拠） | 削除時 |
| BR-60-005 | タイトル優先 | 既存タイトルがある場合はメタデータで上書きしない | 更新時 |

### 計算ロジック

**ページネーション計算**:
```javascript
pages = Math.ceil(count / limit)
prev = page > 1 ? page - 1 : null
next = page < pages ? page + 1 : null
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 追加 | recommendations | INSERT | レコメンデーション作成 |
| 更新 | recommendations | UPDATE | メタデータ更新 |
| 削除 | recommendations | UPDATE | deleted=true |
| クリック記録 | recommendation_click_events | INSERT | クリックイベント |
| 購読記録 | recommendation_subscribe_events | INSERT | 購読イベント |

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

#### recommendations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id, title, url, description, excerpt, featured_image, favicon, one_click_subscribe | 新規作成 | ObjectIDで生成 |
| UPDATE | title, excerpt, featured_image, favicon, one_click_subscribe | メタデータ更新 | URL or IDで特定 |
| SELECT | * | filter, order, limit, page | ページネーション対応 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFoundError | IDが存在しない | 正しいIDを通知 |
| 400 | ValidationError | URL重複 | 既存レコメンデーション通知 |
| 401 | UnauthorizedError | メンバー認証なし（購読時） | 認証を要求 |

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

レコメンデーション操作は単一のsave操作で完結するため、明示的なトランザクション管理は不要です。

## パフォーマンス要件

- 一覧取得: 500ms以内
- メタデータ取得: 5秒タイムアウト

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

- 外部URLへのリクエストにはSSRF対策を実施
- クリックトラッキングはメンバー認証なしでも可能（オプション）
- 購読トラッキングはメンバー認証必須

## 備考

- Well-known URL: `/.well-known/recommendations.json`
- 受信レコメンデーションは `MentionCreatedEvent` 経由で検知

---

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

### 推奨読解順序

#### Step 1: サービス初期化を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | recommendation-service-wrapper.js | `ghost/core/core/server/services/recommendations/recommendation-service-wrapper.js` | init(), 依存関係 |

#### Step 2: コアサービスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/recommendation-service.ts` | addRecommendation, editRecommendation, deleteRecommendation |
| 2-2 | recommendation-controller.ts | `ghost/core/core/server/services/recommendations/service/recommendation-controller.ts` | add, edit, browse, trackClicked |

#### Step 3: 受信レコメンデーションを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | incoming-recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-service.ts` | sendRecommendationEmail |

**主要処理フロー**:
- **Service 55-70行目**: `init()` - 初期化とメタデータ更新スケジュール
- **Service 123-143行目**: `addRecommendation()` - 追加処理
- **Service 199-217行目**: `editRecommendation()` - 更新処理
- **Service 219-236行目**: `deleteRecommendation()` - 削除処理
- **Service 263-266行目**: `trackClicked()` - クリック記録
- **Service 268-271行目**: `trackSubscribed()` - 購読記録
- **Controller 145-169行目**: `browse()` - 一覧取得

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

```
Admin API Request
    │
    └─ RecommendationController
           │
           ├─ add(frame)
           │      └─ RecommendationService.addRecommendation()
           │             ├─ repository.getByUrl() [重複チェック]
           │             ├─ Recommendation.create()
           │             ├─ repository.save()
           │             ├─ updateWellknown()
           │             ├─ updateRecommendationsEnabledSetting()
           │             └─ sendMentionToRecommendation()
           │
           ├─ browse(frame)
           │      └─ RecommendationService.listRecommendations()
           │             └─ repository.getPage()
           │
           └─ trackClicked(frame)
                  └─ RecommendationService.trackClicked()
                         └─ clickEventRepository.save()

Incoming Recommendation:
    │
    └─ MentionCreatedEvent
           │
           └─ isRecommendationUrl() チェック
                  │
                  └─ IncomingRecommendationService.sendRecommendationEmail()
```

### データフロー図

```
┌─────────────────┐     ┌───────────────────────────┐     ┌───────────────────┐
│  Admin API      │────>│ RecommendationController  │────>│ RecommendationSvc │
│  (CRUD)         │     │  (add/edit/browse/delete) │     │  (business logic) │
└─────────────────┘     └───────────────────────────┘     └───────────────────┘
                                                                   │
                                    ┌──────────────────────────────┤
                                    ▼                              ▼
                        ┌───────────────────┐          ┌───────────────────┐
                        │  recommendations  │          │  Well-known       │
                        │  (Database)       │          │  (JSON file)      │
                        └───────────────────┘          └───────────────────┘
                                                                   │
                                                                   ▼
                                                       ┌───────────────────┐
                                                       │  Webmention Send  │
                                                       │  (notify target)  │
                                                       └───────────────────┘

Incoming:
┌─────────────────┐     ┌───────────────────────────┐     ┌───────────────────┐
│  Webmention     │────>│ MentionCreatedEvent       │────>│ Incoming Rec Svc  │
│  (receive)      │     │  (recommendations.json)   │     │  (email notify)   │
└─────────────────┘     └───────────────────────────┘     └───────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| recommendation-service-wrapper.js | `ghost/core/core/server/services/recommendations/recommendation-service-wrapper.js` | ソース | サービス初期化 |
| recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/recommendation-service.ts` | ソース | コアビジネスロジック |
| recommendation-controller.ts | `ghost/core/core/server/services/recommendations/service/recommendation-controller.ts` | ソース | APIコントローラー |
| recommendation.ts | `ghost/core/core/server/services/recommendations/service/recommendation.ts` | ソース | エンティティ |
| incoming-recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-service.ts` | ソース | 受信レコメンデーション |
| incoming-recommendation-controller.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-controller.ts` | ソース | 受信コントローラー |
| well-known-service.ts | `ghost/core/core/server/services/recommendations/service/well-known-service.ts` | ソース | Well-known管理 |
| recommendation-metadata-service.ts | `ghost/core/core/server/services/recommendations/service/recommendation-metadata-service.ts` | ソース | メタデータ取得 |
| click-event.ts | `ghost/core/core/server/services/recommendations/service/click-event.ts` | ソース | クリックイベント |
| subscribe-event.ts | `ghost/core/core/server/services/recommendations/service/subscribe-event.ts` | ソース | 購読イベント |
