# 機能設計書 21-メールテンプレート

## 概要

本ドキュメントは、Ghostのニュースレター配信におけるメールテンプレート機能の設計仕様を記述する。メールテンプレート機能は、ニュースレターのデザイン・レイアウトのカスタマイズを可能にし、ブランドアイデンティティに合致した一貫性のあるメール配信を実現する。

### 本機能の処理概要

**業務上の目的・背景**：パブリッシャーやコンテンツクリエイターにとって、ニュースレターのデザインはブランド認知を高め、購読者との関係性を構築する重要な要素である。メールテンプレート機能は、HTMLメールのレイアウト、配色、フォント、ヘッダー画像などをカスタマイズ可能にし、サイトのブランディングとの一貫性を保つことで、購読者のエンゲージメント向上に貢献する。

**機能の利用シーン**：
- ニュースレターのブランディングカスタマイズ（ロゴ、配色、フォント設定）
- 記事公開時のメール配信フォーマット設定
- プレビュー機能によるデザイン確認
- テストメール送信によるレイアウト検証

**主要な処理内容**：
1. ニュースレター設定に基づくHTMLメールテンプレートのレンダリング
2. 記事コンテンツ（Lexical/Mobiledoc形式）のHTML変換
3. メンバー情報に基づく動的プレースホルダー置換（名前、メールアドレス、購読状態等）
4. レスポンシブデザイン対応（デスクトップ/モバイル）
5. メールクライアント互換性のためのCSSインライン化
6. プレーンテキスト版の自動生成

**関連システム・外部連携**：
- Mailgun（メール配信プロバイダー）
- Handlebars（テンプレートエンジン）
- Juice（CSSインライン化ライブラリ）
- Lexical/Mobiledocレンダラー

**権限による制御**：管理者（Administrator）以上のロールを持つスタッフユーザーがニュースレター設定を変更可能。メンバーは自身の購読設定のみ変更可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 50 | ニュースレター設定 | 主機能 | ニュースレターの作成・デザイン設定 |
| 21 | メールテンプレート設定 | 補助機能 | メールテンプレートのカスタマイズ |

## 機能種別

テンプレートレンダリング / データ変換 / 動的コンテンツ生成

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| post | Post | Yes | 配信対象の記事オブジェクト | lexical または mobiledoc が必須 |
| newsletter | Newsletter | Yes | ニュースレター設定オブジェクト | status が 'active' であること |
| segment | string | No | 配信対象セグメント（'status:free' / 'status:-free'） | 有効なセグメント識別子 |
| options.clickTrackingEnabled | boolean | No | クリックトラッキングの有効/無効 | - |

### 入力データソース

- **newsletters テーブル**: ニュースレターのデザイン設定（背景色、フォント、ヘッダー画像等）
- **posts テーブル**: 配信対象記事のコンテンツ（lexical/mobiledoc）
- **settings テーブル**: サイト全体の設定（アクセントカラー、タイトル、アイコン等）
- **members テーブル**: メンバー情報（プレースホルダー置換用）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| html | string | インライン化されたHTML形式のメール本文 |
| plaintext | string | プレーンテキスト形式のメール本文 |
| replacements | ReplacementDefinition[] | メンバー固有プレースホルダーの置換定義配列 |
| subject | string | メールの件名 |

### 出力先

- Mailgun API（メール配信）
- メールクライアント（最終表示）

## 処理フロー

### 処理シーケンス

```
1. 記事コンテンツのHTML変換
   └─ Lexical/Mobiledocレンダラーで記事本文をHTML化

2. セグメント別コンテンツ処理
   └─ 有料会員限定コンテンツの制御（Paywall処理）
   └─ data-gh-segment 属性による条件付きコンテンツ表示

3. テンプレートデータの構築
   └─ サイト設定、ニュースレター設定、記事情報の収集
   └─ 色・フォント・レイアウト設定の適用

4. Handlebarsテンプレートのレンダリング
   └─ パーシャル（styles, paywall, feedback-button, latest-posts）の組み込み

5. リンク処理
   └─ クリックトラッキング用URLへの変換
   └─ 会員帰属追跡パラメータの付与

6. CSSインライン化
   └─ Juiceライブラリによるスタイルのインライン化

7. メールクライアント互換性対応
   └─ Outlook向け特殊文字エスケープ
   └─ figure/figcaption → div 変換

8. プレースホルダー定義の構築
   └─ メンバー固有値（uuid, name, email等）の置換ルール定義

9. プレーンテキスト版の生成
   └─ HTMLからプレーンテキストへの変換
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B[記事コンテンツ取得]
    B --> C{Lexical形式?}
    C -->|Yes| D[Lexicalレンダラーで変換]
    C -->|No| E[Mobiledocレンダラーで変換]
    D --> F[セグメント処理]
    E --> F
    F --> G{有料限定コンテンツあり?}
    G -->|Yes| H{無料セグメント?}
    H -->|Yes| I[Paywall挿入・有料コンテンツ削除]
    H -->|No| J[全コンテンツ表示]
    G -->|No| J
    I --> K[テンプレートデータ構築]
    J --> K
    K --> L[Handlebarsレンダリング]
    L --> M{クリックトラッキング有効?}
    M -->|Yes| N[リンクにトラッキングURL付与]
    M -->|No| O[相対URLを絶対URLに変換]
    N --> P[CSSインライン化]
    O --> P
    P --> Q[メールクライアント互換性対応]
    Q --> R[プレースホルダー定義生成]
    R --> S[プレーンテキスト生成]
    S --> T[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-21-01 | セグメント別コンテンツ | 無料会員向けメールでは有料限定コンテンツをPaywallに置換 | post.visibility が 'paid' または 'tiers' |
| BR-21-02 | プレースホルダーフォールバック | メンバー名が未設定の場合はフォールバック値を使用 | %%{name, "fallback"}%% 形式で指定 |
| BR-21-03 | アーカイブニュースレター制限 | アーカイブ済みニュースレターへの配信は禁止 | newsletter.status が 'archived' |
| BR-21-04 | ローカル開発環境制限 | 開発環境では送信元アドレスを localhost@example.com に書き換え | NODE_ENV !== 'production' |

### 計算ロジック

**アクセントカラーのコントラスト計算**：
- textColorForBackgroundColor 関数で背景色に対する最適なテキスト色（白/黒）を自動計算
- ボタン色、リンク色もアクセントカラーベースで計算

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| テンプレートレンダリング | newsletters | SELECT | デザイン設定の取得 |
| テンプレートレンダリング | posts | SELECT | 記事コンテンツの取得 |
| テンプレートレンダリング | settings | SELECT | サイト設定の取得 |
| テンプレートレンダリング | posts_meta | SELECT | 記事メタ情報の取得 |

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

#### newsletters

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | background_color, header_image, title_font_category, body_font_category, button_corners, button_style, show_badge, feedback_enabled 等 | id で検索 | デザイン設定全般 |

#### posts

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | title, lexical, mobiledoc, feature_image, visibility, published_at | id で検索 | 記事コンテンツ |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | BadRequestError | アーカイブ済みニュースレターへの送信試行 | エラーメッセージを返却 |
| - | EmailError | ニュースレターが関連付けられていない | エラーメッセージを返却 |
| - | HostLimitError | メール送信制限超過 | エラーメッセージを返却 |

### リトライ仕様

テンプレートレンダリング自体はリトライ不要。メール送信失敗時のリトライはBatchSendingServiceが担当。

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

テンプレートレンダリングは読み取り専用のため、トランザクション管理は不要。

## パフォーマンス要件

- 単一メールのレンダリング: 100ms以内
- 画像サイズ取得は非同期で実行し、キャッシュを活用
- 大量配信時はバッチ処理でメモリ使用量を制御

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

- HTMLコンテンツはサニタイズ処理済み
- プレースホルダーは正規表現でバリデーション
- 外部リンクには rel="noopener noreferrer nofollow" を自動付与
- メンバーのuuidはHMACで署名（改ざん防止）

## 備考

- テンプレートはHandlebars形式で、パーシャル機能を活用
- メールクライアント（特にOutlook）の互換性確保が重要
- Lexicalエディタ移行により、Mobiledocサポートは将来的に削除予定

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `ghost/core/core/server/data/schema/schema.js` | newsletters テーブル（12-60行目）のカラム定義を確認。デザイン設定項目の理解 |
| 1-2 | schema.js | `ghost/core/core/server/data/schema/schema.js` | emails テーブル（826-868行目）のカラム定義を確認 |

**読解のコツ**: newslettersテーブルには多数のデザイン設定カラムがある。background_color, header_image, title_font_category等のカラムがテンプレートレンダリングでどう使われるか意識して読む。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | email-service.js | `ghost/core/core/server/services/email-service/email-service.js` | EmailServiceクラスがメール送信の起点。createEmail()メソッドを確認 |

**主要処理フロー**:
1. **139-184行目**: createEmail()でメール作成の起点。EmailRendererを使用してsubject, from, replyToを取得
2. **297-307行目**: previewEmail()でプレビュー生成。renderBody()を呼び出し

#### Step 3: テンプレートレンダリングを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | email-renderer.js | `ghost/core/core/server/services/email-service/email-renderer.js` | EmailRendererクラスがメインのレンダリングロジック |

**主要処理フロー**:
- **327-374行目**: renderPostBaseHtml()で記事のベースHTMLを生成
- **384-577行目**: renderBody()がメインのレンダリング処理。セグメント処理、Paywall挿入、CSSインライン化を実行
- **854-880行目**: renderTemplate()でHandlebarsテンプレートをコンパイル・レンダリング
- **697-848行目**: buildReplacementDefinitions()でプレースホルダー定義を生成
- **1122-1375行目**: getTemplateData()でテンプレートに渡すデータを構築

#### Step 4: テンプレートファイルを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | template.hbs | `ghost/core/core/server/services/email-service/email-templates/template.hbs` | メインのHTMLテンプレート。Handlebars構文とレイアウト構造 |

**主要処理フロー**:
- **1-10行目**: HTMLヘッダー、metaタグ、タイトル設定
- **17-163行目**: ヘッダーセクション（ロゴ、サイト名、記事タイトル、アイキャッチ画像）
- **165-282行目**: メインコンテンツエリア（記事本文、フィードバックボタン、フッター）

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

```
EmailService.createEmail()
    │
    ├─ EmailRenderer.getSubject()
    ├─ EmailRenderer.getFromAddress()
    ├─ EmailRenderer.getReplyToAddress()
    │
    └─ BatchSendingService.scheduleEmail()
           │
           └─ EmailRenderer.renderBody()
                  │
                  ├─ renderPostBaseHtml()
                  │      └─ Lexical/Mobiledoc Renderer
                  │
                  ├─ getTemplateData()
                  │      ├─ limitImageWidth()
                  │      └─ Settings Cache
                  │
                  ├─ renderTemplate()
                  │      └─ Handlebars.compile()
                  │
                  ├─ LinkReplacer.replace()
                  │      └─ LinkTracking.addTrackingToUrl()
                  │
                  ├─ Juice (CSS Inline)
                  │
                  └─ buildReplacementDefinitions()
```

### データフロー図

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

Post (lexical/mobiledoc) ──▶ Lexical/Mobiledoc Renderer ──▶ Base HTML
                                     │
Newsletter Settings ──────────────────▶ getTemplateData() ──▶ Template Data
Site Settings ────────────────────────┘
                                     │
Template Data + Base HTML ──▶ Handlebars Render ──▶ Raw HTML
                                     │
Raw HTML ─────────────────▶ Link Processing ──▶ Tracked HTML
                                     │
Tracked HTML ─────────────▶ Juice CSS Inline ──▶ Final HTML
                                     │
Final HTML ───────────────▶ HTML to Plaintext ──▶ Plaintext

Member Data ──────────────▶ buildReplacementDefinitions() ──▶ Replacements[]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| email-renderer.js | `ghost/core/core/server/services/email-service/email-renderer.js` | ソース | メインのテンプレートレンダリングロジック |
| email-service.js | `ghost/core/core/server/services/email-service/email-service.js` | ソース | メール作成・送信のオーケストレーション |
| template.hbs | `ghost/core/core/server/services/email-service/email-templates/template.hbs` | テンプレート | HTMLメールのメインテンプレート |
| styles.hbs | `ghost/core/core/server/services/email-service/email-templates/partials/styles.hbs` | テンプレート | CSSスタイル定義パーシャル |
| paywall.hbs | `ghost/core/core/server/services/email-service/email-templates/partials/paywall.hbs` | テンプレート | Paywallセクションパーシャル |
| feedback-button.hbs | `ghost/core/core/server/services/email-service/email-templates/partials/feedback-button.hbs` | テンプレート | フィードバックボタンパーシャル |
| latest-posts.hbs | `ghost/core/core/server/services/email-service/email-templates/partials/latest-posts.hbs` | テンプレート | 最新記事セクションパーシャル |
| register-helpers.js | `ghost/core/core/server/services/email-service/helpers/register-helpers.js` | ソース | Handlebarsヘルパー関数定義 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | データベーススキーマ定義 |
