# 通知設計書 17-レコメンデーション受信通知

## 概要

本ドキュメントは、Ghost CMSにおけるレコメンデーション受信通知機能の設計仕様を記載したものである。

### 本通知の処理概要

レコメンデーション受信通知は、他のGhostサイトがあなたのサイトをレコメンデーション（推薦）に追加した際に、スタッフメンバーに対してメール通知を送信する機能である。この通知により、サイト運営者は他サイトからの推薦状況を把握し、相互推薦の機会を発見できる。

**業務上の目的・背景**：Ghostのレコメンデーション機能は、サイト間の相互プロモーションを促進する仕組みである。他のサイトがあなたを推薦すると、そのサイトの新規会員登録時にあなたのサイトが紹介される。この通知により、推薦元サイトを把握し、「推薦返し」（Recommend back）を行うことで相互のオーディエンス拡大に貢献できる。Ghostエコシステム内でのネットワーキング機会を最大化する。

**通知の送信タイミング**：他のサイトがあなたのサイトを推薦リストに追加し、その情報がWebmention経由で検出された際に送信される。具体的には、`IncomingRecommendationService.sendRecommendationEmail`が呼び出されたタイミング。

**通知の受信者**：Ghostの管理画面で「レコメンデーション通知」を有効化しているアクティブなスタッフメンバー全員。ユーザーモデルの`recommendation_notifications`カラムが`true`かつ`status`が`active`のユーザーが対象。

**通知内容の概要**：推薦元サイトのタイトル、URL、説明文、ファビコン、アイキャッチ画像、既に相互推薦しているかどうかの情報が含まれる。

**期待されるアクション**：受信者は推薦元サイトを確認し、「Recommend back」ボタンから相互推薦を設定できる。既に相互推薦している場合は「View recommendations」ボタンでレコメンデーション一覧を確認できる。

## 通知種別

メール通知（スタッフ向け）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | イベント駆動（Webmention検出時） |
| 優先度 | 中 |
| リトライ | なし |

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

`getEmailRecipients()`コールバックにより、以下の条件を満たすユーザーを取得：
- `status:active` - アクティブなユーザーのみ
- `recommendation_notifications:true` - レコメンデーション通知が有効（デフォルトはtrue）

該当する全ユーザーに対してメールを送信する。

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | サイト設定のデフォルトメールアドレス |
| 送信元名称 | サイト名 |
| 件名 | `New recommendation: {推薦元サイト名}` |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

```html
<!-- HTMLテンプレート: recommendation-received.hbs -->
{{#if recommendation.recommendingBack}}
  <h1>A site is now recommending you back</h1>
{{else}}
  <h1>A site is now recommending you to their audience</h1>
  <p>Every time someone new signs up on their site, they'll see an option to check out {{siteTitle}} as well.</p>
{{/if}}

<figure>
  <a href="{{recommendation.url}}">
    {{#if recommendation.favicon}}<img src="{{recommendation.favicon}}">{{/if}}
    <div>{{recommendation.title}}</div>
    {{#if recommendation.excerpt}}
      <div>{{recommendation.excerpt}}</div>
    {{/if}}
  </a>
  {{#if recommendation.featuredImage}}
    <img src="{{recommendation.featuredImage}}">
  {{/if}}
</figure>

{{#if recommendation.recommendingBack}}
  <a href="{{siteUrl}}ghost/#/settings/recommendations">View recommendations</a>
{{else}}
  <a href="{{siteUrl}}ghost/#/settings/recommendations/add?url={{encodeURIComponent recommendation.url}}">Recommend back</a>
{{/if}}
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| recommendation.id | 推薦ID | mention.id | Yes |
| recommendation.title | 推薦元サイトタイトル | mention.sourceSiteTitle \|\| mention.sourceTitle | Yes |
| recommendation.url | 推薦元サイトURL | mention.source（.well-known/recommendations.json を除去） | Yes |
| recommendation.excerpt | サイト説明 | mention.sourceExcerpt | No |
| recommendation.favicon | ファビコンURL | mention.sourceFavicon | No |
| recommendation.featuredImage | アイキャッチ画像URL | mention.sourceFeaturedImage | No |
| recommendation.recommendingBack | 相互推薦フラグ | recommendationService.readRecommendationByUrl() | Yes |
| siteTitle | サイト名 | settingsCache.get('title') | Yes |
| siteUrl | サイトURL | urlUtils.getSiteUrl() | Yes |
| siteDomain | サイトドメイン | URLから抽出 | Yes |
| siteIconUrl | サイトアイコンURL | blogIcon.getIconUrl() | No |
| accentColor | アクセントカラー | settingsCache.get('accent_color') | No |
| staffUrl | スタッフ設定URL | urlUtils.urlFor('admin') + '/settings/staff/{slug}/email-notifications' | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| Webmention | `/.well-known/recommendations.json`へのWebmention | mentionがIncomingRecommendationに変換可能 | 他サイトがレコメンデーションを追加した際 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| mentionがnullに変換 | 不正なWebmentionの場合 |
| 通知対象ユーザーが0人 | recommendation_notifications:trueのユーザーがいない場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Webmention受信] --> B{/.well-known/recommendations.jsonへの言及?}
    B -->|No| Z[処理終了]
    B -->|Yes| C[IncomingRecommendationに変換]
    C --> D{変換成功?}
    D -->|No| Z
    D -->|Yes| E[相互推薦チェック]
    E --> F[通知対象ユーザー取得]
    F --> G{対象ユーザー存在?}
    G -->|No| Z
    G -->|Yes| H[各ユーザーにメール送信]
    H --> Z
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| mentions | Webmention情報取得 | source URLが`/.well-known/recommendations.json`で終わるもの |
| recommendations | 相互推薦チェック | recommendationService経由 |
| users | 通知対象スタッフ取得 | status:active, recommendation_notifications:true |
| settings | サイト設定取得 | title, accent_color |

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

#### mentions（Webmention/IncomingRecommendation）

| 参照項目 | 用途 | 取得条件 |
|---------|------|---------|
| id | 推薦ID | - |
| source | ソースURL | `/.well-known/recommendations.json`を除去してサイトURLを抽出 |
| sourceTitle | ソースタイトル | - |
| sourceSiteTitle | ソースサイト名 | タイトルとして優先使用 |
| sourceExcerpt | ソース説明 | - |
| sourceFavicon | ファビコンURL | - |
| sourceFeaturedImage | アイキャッチ画像URL | - |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | この通知では更新処理なし |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 変換失敗 | mentionToIncomingRecommendationでエラー | logging.errorでログ出力、処理中断 |
| 送信失敗 | SMTP接続エラー等 | エラーをスロー |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

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

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

- 外部サイトからの情報を含むため、表示時のXSS対策が必要
- ファビコンや画像は外部URLを直接参照
- 「Recommend back」リンクにはURLが埋め込まれるため、適切なエンコーディング（encodeURIComponent）を使用

## 備考

- IncomingRecommendationはWebmention経由で検出（`/.well-known/recommendations.json`へのリンク）
- `recommendingBack`フラグにより、既に相互推薦しているかを判定
- 相互推薦の場合は見出しと CTAが変化
- TypeScriptで実装（incoming-recommendation-email-renderer.ts, incoming-recommendation-service.ts）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | incoming-recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-service.ts` | 7-15行目: IncomingRecommendation型定義 |

**読解のコツ**: TypeScriptの型定義により、推薦データの構造が明確に定義されている。`recommendingBack`フラグが重要。

#### Step 2: メンションからの変換

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | incoming-recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-service.ts` | 107-128行目: `mentionToIncomingRecommendation`メソッド |

**主要処理フロー**:
- **109行目**: ソースURLから`/.well-known/recommendations.json`を除去
- **112-113行目**: 相互推薦チェック（`readRecommendationByUrl`）
- **115-123行目**: IncomingRecommendationオブジェクトの構築

#### Step 3: メール送信処理

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | incoming-recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-service.ts` | 130-144行目: `sendRecommendationEmail`メソッド |
| 3-2 | incoming-recommendation-email-renderer.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts` | 20-36行目: レンダラー（件名、HTML、テキスト生成） |

**主要処理フロー**:
- **131-134行目**: mentionをIncomingRecommendationに変換
- **135行目**: メール受信者取得
- **137-142行目**: 各受信者へメール送信

#### Step 4: テンプレート

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | recommendation-received.hbs | `ghost/core/core/server/services/staff/email-templates/recommendation-received.hbs` | 全体: recommendingBackによる分岐 |

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

```
Webmention検出（/.well-known/recommendations.json）
    │
    └─ IncomingRecommendationService.sendRecommendationEmail(mention)
           │
           ├─ #mentionToIncomingRecommendation(mention)
           │      ├─ URL変換（.well-known/recommendations.json除去）
           │      └─ recommendationService.readRecommendationByUrl() - 相互推薦チェック
           │
           ├─ #getEmailRecipients() - 通知対象取得
           │
           └─ for (recipient of recipients)
                  ├─ #emailRenderer.renderSubject()
                  ├─ #emailRenderer.renderHTML()
                  │      └─ staffService.api.emails.renderHTML('recommendation-received')
                  ├─ #emailRenderer.renderText()
                  │      └─ staffService.api.emails.renderText('recommendation-received')
                  └─ #emailService.send()
```

### データフロー図

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

Webmention（mention）───▶ IncomingRecommendationService ───▶ スタッフメンバーへのメール
  │                           │
  ├─ source                  ├─ URL変換
  ├─ sourceTitle             ├─ 相互推薦チェック
  ├─ sourceSiteTitle         ├─ IncomingRecommendation構築
  ├─ sourceExcerpt           ├─ テンプレートレンダリング
  ├─ sourceFavicon           └─ メール送信
  └─ sourceFeaturedImage

[出力メール]
  ├─ recommendingBack=true: 「推薦返しありがとう」メッセージ
  └─ recommendingBack=false: 「推薦返し」促進メッセージ
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| incoming-recommendation-service.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-service.ts` | ソース | メイン処理ロジック |
| incoming-recommendation-email-renderer.ts | `ghost/core/core/server/services/recommendations/service/incoming-recommendation-email-renderer.ts` | ソース | メールレンダリング |
| recommendation-received.hbs | `ghost/core/core/server/services/staff/email-templates/recommendation-received.hbs` | テンプレート | HTMLメールテンプレート |
| recommendation-received.txt.js | `ghost/core/core/server/services/staff/email-templates/recommendation-received.txt.js` | テンプレート | テキストメールテンプレート |
