# 画面設計書 12-ページ一覧画面

## 概要

本ドキュメントは、Ghost管理画面における固定ページ一覧画面の設計仕様を定義するものである。

### 本画面の処理概要

ページ一覧画面は、サイトの固定ページ（Page）を一覧表示し、管理・編集するための画面である。投稿一覧画面とほぼ同じ構造を持つが、固定ページ専用の機能とUIを提供する。

**業務上の目的・背景**：Webサイトには、時系列で表示される「投稿」とは別に、「About」「お問い合わせ」「プライバシーポリシー」など、固定的に配置されるページが必要である。本画面は、これらの固定ページを効率的に管理するためのインターフェースを提供する。投稿とは異なり、ニュースレター配信機能は存在せず、サイト内の静的コンテンツとして機能する。

**画面へのアクセス方法**：サイドバーの「Pages」メニューをクリック、またはURL `/ghost/#/pages` で直接アクセス可能。

**主要な操作・処理内容**：
1. 固定ページ一覧の表示・無限スクロールによる追加読み込み
2. ステータス別フィルタリング（下書き・公開済み・予約・おすすめ）
3. 公開範囲別フィルタリング（全公開・パブリック・メンバー限定・有料メンバー限定）
4. 著者・タグによるフィルタリング
5. ソート順の変更（新しい順・古い順・更新順）
6. 新規ページ作成への遷移
7. 複数選択による一括操作
8. エディタ画面への遷移

**画面遷移**：
- 遷移元: サイドバーメニュー
- 遷移先: エディタ画面（lexical-editor）、メンバー一覧画面（members）

**権限による表示制御**：
- Contributor: 自分のページのみ表示、著者フィルタ非表示、一括選択機能無効
- Author: 自分のページのみ表示、著者フィルタ非表示
- Editor/Administrator/Owner: 全ページ表示、全機能利用可能

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 2 | ページ管理 | 主機能 | 固定ページの一覧表示・フィルタリング |

## 画面種別

一覧

## URL/ルーティング

| 項目 | 値 |
|------|-----|
| URL | `/ghost/#/pages` |
| ルート名 | pages |
| ルートファイル | `ghost/admin/app/routes/pages.js` |
| 継承元 | `ghost/admin/app/routes/posts.js` |

### クエリパラメータ

| パラメータ名 | 型 | デフォルト値 | 説明 |
|-------------|-----|-------------|------|
| type | string | null | ステータスフィルタ（draft/published/scheduled/featured） |
| visibility | string | null | 公開範囲フィルタ（public/members/[paid,tiers]） |
| author | string | null | 著者スラッグによるフィルタ |
| tag | string | null | タグスラッグによるフィルタ |
| order | string | null | ソート順（published_at asc/updated_at desc） |

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 必須 | 型 | 説明 |
|--------|--------|------|-----|------|
| ステータスフィルタ | type | - | select | ページステータスによる絞り込み |
| 公開範囲フィルタ | visibility | - | select | アクセス権限による絞り込み |
| 著者フィルタ | author | - | select | 著者による絞り込み |
| タグフィルタ | tag | - | select | タグによる絞り込み |
| ソート順 | order | - | select | 表示順の指定 |

### フィルタ選択肢

#### ステータスフィルタ（type）

| 表示名 | 値 |
|--------|-----|
| All pages | null |
| Draft pages | draft |
| Published pages | published |
| Scheduled pages | scheduled |
| Featured pages | featured |

**注意**: 投稿一覧とは異なり、「Email only posts（sent）」のオプションは存在しない。

#### 公開範囲フィルタ（visibility）

| 表示名 | 値 |
|--------|-----|
| All access | null |
| Public | public |
| Members-only | members |
| Paid members-only | [paid,tiers] |

#### ソート順（order）

| 表示名 | 値 |
|--------|-----|
| Newest first | null（デフォルト: published_at desc） |
| Oldest first | published_at asc |
| Recently updated | updated_at desc |

## 表示項目

### ページリスト項目

| 項目名 | フィールド | 説明 |
|--------|-----------|------|
| タイトル | title | ページのタイトル |
| おすすめアイコン | featured | おすすめページの場合はスターアイコン表示 |
| 著者名 | authors | 「By {著者名}」形式で表示 |
| プライマリタグ | primaryTag | 「in {タグ名}」形式で表示 |
| 日付 | publishedAtUTC/updatedAtUTC | 下書き・予約は更新日、公開済みは公開日 |
| ステータス | status | Draft/Scheduled/Published |
| 訪問者数 | visitorCount | Webアナリティクス有効時に表示 |
| メンバー獲得数 | memberCounts | メンバートラッキング有効時に表示 |

**注意**: 投稿一覧とは異なり、メール関連のメトリクス（開封率・クリック率）は表示されない。

### 空状態表示

| 条件 | 表示内容 |
|------|---------|
| ページなし（フィルタなし） | 「Tell the world about yourself.」メッセージと新規作成ボタン |
| ページなし（フィルタあり） | 「No pages match the current filter」と全表示ボタン |

## イベント仕様

### 1-新規ページボタン押下

- 処理: Lexicalエディタの新規ページ作成画面へ遷移
- 遷移先ルート: `lexical-editor.new`
- 遷移パラメータ: `page`

### 2-ページ行クリック

- 処理: 該当ページのエディタ画面へ遷移
- 遷移先ルート: `lexical-editor.edit`
- 遷移パラメータ: `['page', page.id]`
- 例外: Contributorかつ公開済みの場合は外部リンクとしてページURLを開く

### 3-フィルタ変更

- 処理: クエリパラメータを更新し、ページ一覧を再取得
- データ再取得: InfinityModelをリセットして再フェッチ
- スクロール位置: 一覧の先頭にリセット

### 4-無限スクロール

- 処理: スクロール位置がトリガーオフセット（1000px）に達したら次ページを読み込み
- ページサイズ: 30件/ページ（PostsRouteから継承）
- 読み込み順序: scheduled → draft → published

### 5-コンテキストメニュー操作

- 右クリックまたは複数選択時にコンテキストメニュー表示
- 利用可能操作: タグ追加、アクセス権変更、非公開化、予約解除、削除

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ページ一覧表示 | posts | SELECT | ページデータの取得（type='page'） |
| ページ一覧表示 | users | SELECT | 著者情報の取得 |
| ページ一覧表示 | tags | SELECT | タグ情報の取得 |
| フィルタ用タグ取得 | tags | SELECT | フィルタ選択肢用タグ一覧取得 |
| フィルタ用著者取得 | users | SELECT | フィルタ選択肢用ユーザー一覧取得 |

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

#### posts（type='page'）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | id, title, status, visibility, featured, slug, uuid | filter条件に基づく + type='page' | 無限スクロール対応 |
| SELECT | published_at, updated_at, created_at | ソート順に使用 | |
| SELECT | authors, tags | リレーション含む | embedded: always |

## メッセージ仕様

| メッセージID | 種別 | 条件 | メッセージ内容 |
|-------------|------|------|---------------|
| MSG-01 | 情報 | ページが0件（フィルタなし） | Tell the world about yourself. |
| MSG-02 | 情報 | ページが0件（フィルタあり） | No pages match the current filter |

## 例外処理

| 例外ケース | 処理内容 |
|-----------|---------|
| API通信エラー | 標準のエラーハンドリングによるエラー表示 |
| 権限不足 | 権限に応じたフィルタ・機能の非表示 |
| 不明なフィルタ値 | 「Unknown type/visibility/author/tag」と赤字で表示 |

## 備考

- PagesRouteはPostsRouteを継承しており、modelName='page'を設定するだけで固定ページ用として機能する
- PagesControllerはPostsControllerを継承し、TYPES配列をページ用に再定義している
- PageモデルはPostモデルを継承し、displayName='page'を設定するだけで定義されている
- 投稿との主な違い：
  - ニュースレター配信機能なし（'sent'ステータスなし）
  - メール関連メトリクス非表示
  - 空状態メッセージが異なる

---

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

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

### 推奨読解順序

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

ページはPostモデルを継承しているため、まずPostモデルを理解し、次にPageモデルの差分を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | post.js | `ghost/admin/app/models/post.js` | 基底モデルの属性定義 |
| 1-2 | page.js | `ghost/admin/app/models/page.js` | displayName='page'の設定のみ |

**読解のコツ**: PageはPostをそのまま継承しており、displayNameプロパティでタイプを識別している。

**主要処理フロー（page.js）**:
- **1-5行目**: PostModelの継承とdisplayName設定のみ

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

PagesRouteはPostsRouteを継承している。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | posts.js | `ghost/admin/app/routes/posts.js` | 基底ルートの完全な実装 |
| 2-2 | pages.js | `ghost/admin/app/routes/pages.js` | modelName='page'の設定とメタデータのみ |

**主要処理フロー（pages.js）**:
- **4-7行目**: PostsRouteの継承とmodelName='page'設定
- **9-12行目**: titleToken='Pages'の設定

#### Step 3: コントローラーを理解する

PagesControllerはPostsControllerを継承し、フィルタ選択肢を上書きしている。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | posts.js | `ghost/admin/app/controllers/posts.js` | 基底コントローラーの完全な実装 |
| 3-2 | pages.js | `ghost/admin/app/controllers/pages.js` | TYPESとORDERSの上書き |

**主要処理フロー（pages.js）**:
- **5-20行目**: ページ用のTYPES定義（sentオプションなし）
- **22-31行目**: ページ用のORDERS定義
- **33-37行目**: PostsControllerの継承とプロパティ上書き
- **39-42行目**: openEditorアクションの上書き（'page'タイプ指定）

#### Step 4: テンプレートを理解する

投稿一覧とほぼ同じ構造だが、ページ固有の設定がある。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | pages.hbs | `ghost/admin/app/templates/pages.hbs` | 画面レイアウト、投稿テンプレートとの差分 |

**主要処理フロー**:
- **3行目**: タイトル「Pages」固定
- **28行目**: 新規ページ作成ボタン（model='page'）
- **40-44行目**: 空状態メッセージ「Tell the world about yourself.」
- **42-44行目**: 新規作成リンク（model='page'）

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

```
PagesRoute (ghost/admin/app/routes/pages.js)
    │
    └─ extends PostsRoute
           │
           ├─ modelName = 'page'（上書き）
           │
           ├─ model() - PostsRouteから継承
           │      ├─ infinity.model('page', params) × 3
           │      └─ RSVP.hash(models)
           │
           └─ setupController() - PostsRouteから継承

PagesController (ghost/admin/app/controllers/pages.js)
    │
    └─ extends PostsController
           │
           ├─ availableTypes = TYPES（ページ用に上書き）
           ├─ availableOrders = ORDERS（上書き）
           └─ openEditor() - 'page'タイプで遷移

pages.hbs (テンプレート)
    │
    ├─ GhCanvasHeader
    │      ├─ GhCustomViewTitle @title="Pages"
    │      └─ PostsList::ContentFilter（投稿と共通）
    │
    ├─ PostsList::List（投稿と共通コンポーネント）
    │      └─ PostsList::ListItemAnalytics
    │
    └─ GhInfinityLoader × 3
```

### データフロー図

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

URLクエリパラメータ ───────▶ PagesRoute.model() ──────────────▶ InfinityModel × 3
(?type=draft&tag=about)          │                                (type='page')
                                 └─ PostsRoute.model()を使用
                                    ただしmodelName='page'

フィルタUI選択 ─────────────▶ PagesController ─────────────────▶ クエリパラメータ更新
                                  │
                                  └─ PostsControllerを継承
                                     ただしTYPESが異なる

InfinityModel ─────────────▶ pages.hbs ───────────────────────▶ PostsList::ListItem
(pages配列)                       │
                                  └─ PostsList::Listを使用
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| pages.js | `ghost/admin/app/routes/pages.js` | ルート | PostsRoute継承、modelName設定 |
| pages.js | `ghost/admin/app/controllers/pages.js` | コントローラー | PostsController継承、フィルタ上書き |
| pages.hbs | `ghost/admin/app/templates/pages.hbs` | テンプレート | 画面レイアウト |
| page.js | `ghost/admin/app/models/page.js` | モデル | PostModel継承、displayName設定 |
| posts.js | `ghost/admin/app/routes/posts.js` | ルート（基底） | データ取得ロジック |
| posts.js | `ghost/admin/app/controllers/posts.js` | コントローラー（基底） | 状態管理ロジック |
| post.js | `ghost/admin/app/models/post.js` | モデル（基底） | データ構造定義 |
| list.js | `ghost/admin/app/components/posts-list/list.js` | コンポーネント | リストロジック（共通） |
| list-item.js | `ghost/admin/app/components/posts-list/list-item.js` | コンポーネント | アイテムロジック（共通） |
| content-filter.hbs | `ghost/admin/app/components/posts-list/content-filter.hbs` | テンプレート | フィルタUI（共通） |
