# 通知設計書 16-メンションレポート

## 概要

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

### 本通知の処理概要

メンションレポートは、他のウェブサイトからWebmentionを受け取った際に、定期的にスタッフメンバーに対してまとめてレポートメールを送信する機能である。この通知により、サイト運営者は他サイトからの言及・リンクをまとめて把握し、被リンク状況やコンテンツの影響力を確認できる。

**業務上の目的・背景**：Webmentionはインターネット上での相互リンクを通知するオープンスタンダードである。他のウェブサイトがあなたのコンテンツにリンクした際、その情報を受け取ることができる。これにより、コンテンツがどこで引用・言及されているかを把握し、ネットワーキングや関係構築の機会を発見できる。レポート形式で定期的に送信することで、通知の過多を防ぎつつ重要な情報を逃さない。

**通知の送信タイミング**：24時間ごとにスケジュールされたジョブが実行され、前回レポート送信以降に受信したWebmentionがある場合に送信される。ランダムな分・秒でスケジュールされ、サーバー負荷を分散する。また、Ghost起動時にも一度実行される。

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

**通知内容の概要**：受信したWebmentionの一覧（最大5件）、各メンションのソースURL、タイトル、抜粋、ファビコン、アイキャッチ画像、サイト名が含まれる。追加のメンションがある場合は「View all」リンクから管理画面で確認できる。

**期待されるアクション**：受信者は各メンションのリンクをクリックして言及元サイトを確認し、必要に応じてフォローバックや返信などのエンゲージメントを行う。「View all mentions」ボタンから管理画面で全メンションを確認できる。

## 通知種別

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

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | スケジュールジョブ（cronライク）+ 起動時チェック |
| 優先度 | 低（バッチ処理） |
| リトライ | なし（24時間後の次回実行で再試行） |

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

`models.User.getEmailAlertUsers('mention-received')` メソッドにより、以下の条件を満たすユーザーを取得：
- `status:active` - アクティブなユーザーのみ
- `mention_notifications:true` - メンション通知が有効（デフォルトはtrue）

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

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | GhostMailerのデフォルト設定 |
| 送信元名称 | サイト名 |
| 件名 | 動的生成（1件: `{サイト名} mentioned you`、2件: `{サイト名} & 1 other mentioned you`、3件以上: `{サイト名} & {N-1} others mentioned you`） |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

```html
<!-- HTMLテンプレート: mention-report.hbs -->
<p>Hey there,</p>
<p>{{siteTitle}} was mentioned {{#eq mentions.length 1}}in:{{else}}<strong>{{mentions.length}} times</strong> recently. Here's where:{{/eq}}</p>

{{#each (limit mentions 5)}}
<figure>
  <a href="{{mention.sourceUrl}}">
    <div>{{mention.sourceTitle}}</div>
    <div>{{mention.sourceExcerpt}}</div>
    <div>
      {{#if mention.sourceFavicon}}<img src="{{mention.sourceFavicon}}">{{/if}}
      {{mention.sourceSiteTitle}}
    </div>
    {{#if mention.sourceFeaturedImage}}
      <img src="{{mention.sourceFeaturedImage}}">
    {{/if}}
  </a>
</figure>
{{/each}}

<a href="{{siteUrl}}ghost/#/mentions">View all {{#if hasMoreMentions}}{{mentions.length}}{{/if}} mentions</a>
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| mentions | メンション配列（最大5件表示） | mentionReportGenerator.getMentionReport() | Yes |
| mentions[].sourceUrl | ソースURL | mention.source | Yes |
| mentions[].targetUrl | ターゲットURL | mention.target | Yes |
| mentions[].sourceTitle | ソースタイトル | mention.sourceTitle | No |
| mentions[].sourceExcerpt | ソース抜粋 | mention.sourceExcerpt | No |
| mentions[].sourceSiteTitle | ソースサイト名 | mention.sourceSiteTitle | No |
| mentions[].sourceFavicon | ファビコンURL | mention.sourceFavicon | No |
| mentions[].sourceAuthor | ソース著者 | mention.sourceAuthor | No |
| mentions[].sourceFeaturedImage | アイキャッチ画像URL | mention.sourceFeaturedImage | No |
| hasMoreMentions | 5件以上のメンションがあるか | report.mentions.length > 5 | Yes |
| siteTitle | サイト名 | settingsCache.get('title') | Yes |
| siteUrl | サイトURL | urlUtils.getSiteUrl() | Yes |
| siteDomain | サイトドメイン | URLから抽出 | Yes |
| accentColor | アクセントカラー | settingsCache.get('accent_color') | No |
| staffUrl | スタッフ設定URL | urlUtils.urlFor('admin') + '/settings/staff/{slug}/email-notifications' | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| スケジュールジョブ | `${s} ${m} * * * *`（ランダム分秒で毎時） | 前回レポートから24時間以上経過 AND 新規メンションあり | 毎時チェック、24時間間隔で送信 |
| 起動時 | Ghost起動時（15秒〜5分後） | 前回レポートから24時間以上経過 AND 新規メンションあり | 起動時の欠落レポート送信 |
| ドメインイベント | StartMentionEmailReportJob | labs.webmentionsが有効 | 明示的なジョブ開始 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 前回レポートから24時間未満 | 頻繁なメール送信を防止 |
| 新規メンションが0件 | レポートすべき内容がない場合 |
| labs.webmentionsが無効 | Webmention機能がオフの場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[スケジュールジョブ/起動時] --> B{labs.webmentions有効?}
    B -->|No| Z[処理終了]
    B -->|Yes| C[前回レポート日時取得]
    C --> D{24時間以上経過?}
    D -->|No| Z
    D -->|Yes| E[メンションレポート取得]
    E --> F{新規メンションあり?}
    F -->|No| Z
    F -->|Yes| G[重複ソースURLを除去]
    G --> H[通知対象ユーザー取得]
    H --> I[各ユーザーにメール送信]
    I --> J[前回レポート日時を更新]
    J --> Z
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| mentions | Webmention情報取得 | mentions.api.getMentionReport()経由 |
| users | 通知対象スタッフ取得 | status:active, mention_notifications:true |
| settings | 前回レポート日時取得 | last_mentions_report_email_timestamp |

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

#### mentions（Webmention）

| 参照項目 | 用途 | 取得条件 |
|---------|------|---------|
| source | ソースURL | 期間指定 |
| target | ターゲットURL | - |
| sourceTitle | ソースタイトル | - |
| sourceExcerpt | ソース抜粋 | - |
| sourceSiteTitle | ソースサイト名 | - |
| sourceFavicon | ファビコンURL | - |
| sourceAuthor | ソース著者 | - |
| sourceFeaturedImage | アイキャッチ画像URL | - |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| settings | UPDATE | last_mentions_report_email_timestampを更新 |

#### 送信ログ更新

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | last_mentions_report_email_timestamp | 現在日時のタイムスタンプ | レポート送信完了時 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | SMTP接続エラー等 | 次回ジョブ実行時に再試行 |
| 設定値不正 | last_mentions_report_email_timestampがNaN | 1日前の日時をデフォルトとして使用 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0（次回スケジュール実行で再試行） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 該当なし（バッチ処理） |
| 1日あたり上限 | 1回（24時間間隔） |

### 配信時間帯

毎時ランダムな分・秒でチェック、24時間経過後に送信

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

- 外部サイトからのWebmention情報を含むため、表示時のXSS対策が必要
- ファビコンや画像は外部URLを直接参照するため、悪意あるコンテンツの可能性あり
- 送信先はスタッフメンバーのみに限定

## 備考

- `labs.isSet('webmentions')`がtrueの場合のみ機能
- スケジュールは`${s} ${m} * * * *`形式で、s（0-59）とm（0-59）はランダム
- 重複ソースURLは除去される（同一ソースからの複数メンションをまとめる）
- 表示は最大5件、それ以上は管理画面で確認

---

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

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

### 推奨読解順序

#### Step 1: サービス初期化とジョブスケジューリング

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | service.js | `ghost/core/core/server/services/mentions-email-report/service.js` | 10-167行目: 初期化処理とジョブスケジューリング |

**読解のコツ**:
- **155-156行目**: ランダムな秒・分でスケジュール生成
- **159-163行目**: mentionsJobsにジョブ登録
- **146-149行目**: DomainEvents購読でlabs.webmentionsをチェック

#### Step 2: レポート生成ジョブ

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | mention-email-report-job.js | `ghost/core/core/server/services/mentions-email-report/mention-email-report-job.js` | 38-78行目: `sendLatestReport`メソッド |

**主要処理フロー**:
- **39-40行目**: 前回レポート日時取得と現在時刻
- **42-44行目**: 24時間チェック
- **46行目**: レポート取得
- **48-58行目**: メンションデータの変換
- **61-62行目**: メンションなしなら終了
- **65-72行目**: 各受信者へメール送信
- **75行目**: 最新レポート日時を更新

#### Step 3: テンプレート処理

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | service.js | `ghost/core/core/server/services/mentions-email-report/service.js` | 37-94行目: mentionReportEmailView（件名生成、テンプレートレンダリング） |
| 3-2 | mention-report.hbs | `ghost/core/core/server/services/staff/email-templates/mention-report.hbs` | HTMLテンプレート全体 |

**主要処理フロー**:
- **41-57行目**: renderSubject - ユニークなサイト名をカウントして件名生成
- **67-68行目**: 重複ソースURLを除去

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

```
mentionsJobs.addJob() - スケジュールジョブ登録
    │
    └─ job.js - ジョブエントリーポイント
           │
           └─ MentionEmailReportJob.sendLatestReport()
                  │
                  ├─ mentionReportHistoryService.getLatestReportDate()
                  │      └─ settingsCache.get('last_mentions_report_email_timestamp')
                  │
                  ├─ mentionReportGenerator.getMentionReport(startDate, endDate)
                  │      └─ mentions.api.getMentionReport()
                  │
                  ├─ mentionReportRecipientRepository.getMentionReportRecipients()
                  │      └─ models.User.getEmailAlertUsers('mention-received')
                  │
                  ├─ mentionReportEmailView.renderSubject/renderHTML/renderText()
                  │      └─ staffService.api.emails.renderHTML('mention-report')
                  │
                  ├─ emailService.send()
                  │      └─ mailer.send()
                  │
                  └─ mentionReportHistoryService.setLatestReportDate()
                         └─ models.Settings.edit()
```

### データフロー図

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

スケジュールトリガー ───▶ MentionEmailReportJob ───▶ スタッフメンバーへのメール
                              │
                              ├─ 前回レポート日時取得
                              ├─ 24時間経過チェック
                              ├─ Webmention取得
                              ├─ 重複除去
                              ├─ 件名動的生成
                              ├─ テンプレートレンダリング
                              └─ 前回レポート日時更新

[データソース]
  ├─ mentions テーブル
  ├─ settings テーブル（last_mentions_report_email_timestamp）
  └─ users テーブル
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| service.js | `ghost/core/core/server/services/mentions-email-report/service.js` | ソース | サービス初期化、依存性注入 |
| mention-email-report-job.js | `ghost/core/core/server/services/mentions-email-report/mention-email-report-job.js` | ソース | レポート生成・送信ロジック |
| job.js | `ghost/core/core/server/services/mentions-email-report/job.js` | ソース | スケジュールジョブエントリーポイント |
| start-mention-email-report-job.js | `ghost/core/core/server/services/mentions-email-report/start-mention-email-report-job.js` | ソース | ジョブ開始イベント |
| mention-report.hbs | `ghost/core/core/server/services/staff/email-templates/mention-report.hbs` | テンプレート | HTMLメールテンプレート |
| mention-report.txt.js | `ghost/core/core/server/services/staff/email-templates/mention-report.txt.js` | テンプレート | テキストメールテンプレート |
