# 画面設計書 91-レコメンデーションページ

## 概要

本ドキュメントは、Ghost会員向け公開画面（Portal）における「レコメンデーションページ」の画面設計書です。サイト運営者が設定したおすすめサイト（Recommendations）を会員に表示し、ワンクリック購読機能を提供する画面です。

### 本画面の処理概要

レコメンデーションページは、Ghost会員に対してサイト運営者が推薦する外部サイトのリストを表示する公開画面です。

**業務上の目的・背景**：サイト運営者がコミュニティ形成や関連サイトとの相互推薦（クロスプロモーション）を促進するために使用します。会員サインアップ直後に表示することで、関連コンテンツへの導線を提供し、エコシステム全体の成長に貢献します。また、外部サイトへのワンクリック購読機能により、会員のスムーズな体験を実現します。

**画面へのアクセス方法**：
- URLハッシュ経由: `/#/portal/recommendations`
- サインアップ成功後の自動リダイレクト（`?action=signup&success=true`クエリパラメータ付き）
- `data-portal="recommendations"`属性を持つカスタムトリガーボタンのクリック

**主要な操作・処理内容**：
1. おすすめサイト一覧の表示（favicon、タイトル、説明文）
2. サイトクリックによる外部サイトへの遷移（新規タブ）
3. ワンクリック購読ボタンによる外部Ghostサイトへの自動登録
4. 「Show all」ボタンによる全件表示
5. 「Maybe later」ボタンによるポップアップの閉じる操作（サインアップ直後のみ表示）

**画面遷移**：
- 遷移元: サインアップ成功画面、カスタムトリガーボタン
- 遷移先: 外部サイト（新規タブ）、ポップアップを閉じて元の画面に戻る

**権限による表示制御**：
- ワンクリック購読ボタンはログイン済み会員のみに表示
- 未ログインユーザーでもレコメンデーション一覧の閲覧は可能

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 60 | サイトレコメンデーション | 主機能 | おすすめサイトの表示・クリックトラッキング |
| 71 | Portal | 補助機能 | Portalウィジェット内でのUI表示 |

## 画面種別

一覧

## URL/ルーティング

- ハッシュルート: `#/portal/recommendations`
- アクセストリガー: `data-portal="recommendations"` 属性

## 入出力項目

| 項目名 | 入出力 | データ型 | 必須 | 説明 |
|--------|--------|----------|------|------|
| pageData.signup | 入力 | boolean | - | サインアップ直後かどうかのフラグ |
| recommendations | 出力 | Array | - | おすすめサイトのリスト |

## 表示項目

| 項目名 | データ型 | 説明 |
|--------|----------|------|
| site.icon | string | サイトアイコン画像URL |
| site.title | string | サイト名 |
| heading | string | ページタイトル（「Recommendations」または「Welcome to {siteTitle}」） |
| subheading | string | サブタイトル（説明文） |
| recommendation.title | string | おすすめサイトのタイトル |
| recommendation.description | string | おすすめサイトの説明 |
| recommendation.favicon | string | おすすめサイトのfavicon |
| recommendation.featured_image | string | おすすめサイトの特集画像 |
| recommendation.url | string | おすすめサイトのURL |
| recommendation.one_click_subscribe | boolean | ワンクリック購読対応フラグ |

## イベント仕様

### 1-サイトクリック（visitHandler）

おすすめサイトの行をクリックした際に、外部サイトを新規タブで開きます。

- 処理: `window.open(url, '_blank')` で新規タブを開く
- 追加処理: 初回クリック時のみ `trackRecommendationClicked` アクションを実行
- フォールバック: タブがブロックされた場合は `window.location.href` で遷移

### 2-ワンクリック購読（oneClickSubscribeHandler）

「Subscribe」ボタンクリック時に、外部Ghostサイトへのワンクリック登録を実行します。

- 処理: `doAction('oneClickSubscribe', {siteUrl})` を呼び出し
- 成功時: `trackRecommendationSubscribed` アクションを実行し、「Verification link sent, check your inbox」を表示
- 失敗時: 外部サイトのサインアップページへ遷移

### 3-全件表示（showAllRecommendations）

「Show all」ボタンクリック時に、全おすすめサイトを表示します。

- 処理: `setNumToShow(recommendations.length)` で表示件数を全件に設定

### 4-ポップアップを閉じる（Maybe later）

「Maybe later」ボタンクリック時にポップアップを閉じます。

- 処理: `doAction('closePopup')` を実行
- 表示条件: サインアップ直後（`pageData.signup === true`）のみ表示

## データベース更新仕様

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| サイトクリック | recommendation_clicks | INSERT | クリック追跡データの記録（Beacon API経由） |
| ワンクリック購読 | recommendation_subscribes | INSERT | 購読追跡データの記録（Beacon API経由） |

### テーブル別更新項目詳細

#### recommendation_clicks

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | recommendation_id | クリックされたレコメンデーションのID | Beacon APIで送信 |

## メッセージ仕様

| メッセージID | 種別 | 表示条件 | メッセージ内容 |
|-------------|------|---------|--------------|
| MSG001 | 情報 | 購読成功時 | "Verification link sent, check your inbox" |
| MSG002 | 情報 | レコメンデーションなし | "Sorry, no recommendations are available right now." |
| MSG003 | 情報 | サインアップ直後 | "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy." |
| MSG004 | 情報 | 通常表示時 | "Here are a few other sites you may enjoy." |

## 例外処理

| 例外種別 | 発生条件 | 対応処理 |
|---------|---------|---------|
| APIエラー | レコメンデーション取得失敗 | コンソールにエラーを出力し、空リストを表示 |
| ワンクリック購読失敗 | 外部サイトがOCSに対応していない | 外部サイトのサインアップページへ遷移 |
| タブブロック | ポップアップブロッカー作動 | `window.location.href` でページ遷移 |

## 備考

- レコメンデーションは取得時にシャッフルされ、ワンクリック購読対応サイトが先に表示される
- 初期表示は5件、「Show all」で全件表示
- outbound_link_taggingが有効な場合、外部URLに`ref`パラメータが付与される

---

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

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

### 推奨読解順序

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

まず、レコメンデーションデータの構造とPortalアプリケーションの状態管理を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | app-context.js | `apps/portal/src/app-context.js` | AppContextの構造、site/member/pageDataの型定義 |
| 1-2 | api.js | `apps/portal/src/utils/api.js` | recommendations APIの呼び出し方法（121-136行目） |

**読解のコツ**: Portalアプリはreact-context + useReducerパターンではなく、クラスコンポーネントでstateを管理している。

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

処理の起点となるファイル・関数を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | pages.js | `apps/portal/src/pages.js` | ページマッピング（recommendations: RecommendationsPage） |
| 2-2 | App.js | `apps/portal/src/App.js` | fetchLinkData内でのrecommendationsページ遷移判定（498-507行目） |

**主要処理フロー**:
1. **498-507行目**: サインアップ成功時のrecommendationsページ表示判定
2. **900-907行目**: `getPageFromLinkPath`でのrecommendationsルート処理

#### Step 3: メインコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | recommendations-page.js | `apps/portal/src/components/pages/recommendations-page.js` | メインのページコンポーネント（282-375行目） |

**主要処理フロー**:
- **288-303行目**: recommendationsのAPI呼び出しとシャッフル処理
- **148-163行目**: `shuffleRecommendations`関数（Fisher-Yatesシャッフル）
- **189-280行目**: `RecommendationItem`コンポーネント（ワンクリック購読ロジック）
- **216-224行目**: visitHandler（クリックトラッキング）
- **226-248行目**: oneClickSubscribeHandler（ワンクリック購読）

#### Step 4: APIとアクション処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | api.js | `apps/portal/src/utils/api.js` | recommendations APIの実装（170-179行目） |
| 4-2 | actions.js | `apps/portal/src/actions.js` | trackRecommendationClicked/trackRecommendationSubscribedアクション |

**主要処理フロー**:
- **170-174行目**: `trackClicked` - Beacon APIでクリック追跡
- **175-179行目**: `trackSubscribed` - Beacon APIで購読追跡

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

```
App.js (initSetup/fetchData)
    │
    ├─ fetchLinkData() - ルート判定
    │      └─ hasRecommendations() - 表示判定
    │
    └─ PopupModal
           └─ RecommendationsPage
                  │
                  ├─ api.site.recommendations() - データ取得
                  │      └─ shuffleRecommendations() - シャッフル
                  │
                  └─ RecommendationItem (map)
                         │
                         ├─ visitHandler()
                         │      └─ doAction('trackRecommendationClicked')
                         │
                         └─ oneClickSubscribeHandler()
                                └─ doAction('oneClickSubscribe')
                                └─ doAction('trackRecommendationSubscribed')
```

### データフロー図

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

pageData.signup ──────────▶ RecommendationsPage ─────────▶ 見出し表示
                           │
API: /recommendations ────▶ shuffleRecommendations() ───▶ 一覧表示
                           │
ユーザークリック ──────────▶ visitHandler() ─────────────▶ 外部サイト遷移
                           │                              + Beacon送信
                           │
Subscribeボタン ──────────▶ oneClickSubscribeHandler() ─▶ 購読登録
                                                          + Beacon送信
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| recommendations-page.js | `apps/portal/src/components/pages/recommendations-page.js` | ソース | メインページコンポーネント |
| pages.js | `apps/portal/src/pages.js` | ソース | ページルーティング定義 |
| App.js | `apps/portal/src/App.js` | ソース | アプリケーションルート、状態管理 |
| api.js | `apps/portal/src/utils/api.js` | ソース | API呼び出しユーティリティ |
| helpers.js | `apps/portal/src/utils/helpers.js` | ソース | hasRecommendations、getRefDomain等のユーティリティ |
| i18n.js | `apps/portal/src/utils/i18n.js` | ソース | 多言語対応 |
| close-button.js | `apps/portal/src/components/common/close-button.js` | ソース | 閉じるボタンコンポーネント |
| loading-page.js | `apps/portal/src/components/pages/loading-page.js` | ソース | ローディング状態表示 |
