# 機能設計書 159-公開API

## 概要

本ドキュメントは、Chatwootのヘルプセンターにおける「公開API」機能の設計仕様を定義する。この機能により、認証なしでヘルプセンターのポータル、カテゴリ、記事にアクセスできる公開エンドポイントを提供する。

### 本機能の処理概要

**業務上の目的・背景**：
ヘルプセンターは一般ユーザー（顧客）が認証なしでアクセスするナレッジベースである。公開APIにより、ポータルのコンテンツをWebブラウザ、Web Widget、外部アプリケーションから取得できる。SEO対応のためのサイトマップ生成や、記事の閲覧数追跡（トラッキングピクセル）機能も提供する。

**機能の利用シーン**：
- 顧客がヘルプセンターにアクセスして記事を閲覧する場面
- Web Widgetからヘルプ記事を検索・表示する場面
- 検索エンジンがサイトマップを取得してインデックス化する場面
- 外部システムがヘルプ記事を取得して表示する場面

**主要な処理内容**：
1. ポータル情報の表示（show）とサイトマップ生成（sitemap）
2. カテゴリ一覧・詳細の表示
3. 記事一覧・詳細の表示と全文検索
4. 記事の閲覧数追跡（トラッキングピクセル）
5. ロケール切り替えとリダイレクト

**関連システム・外部連携**：
- 検索エンジン：サイトマップによるクローリング
- Web Widget：ヘルプ記事の表示
- CDN：トラッキングピクセルのキャッシュ

**権限による制御**：
- 全て公開アクセス（認証不要）
- アーカイブされたポータルは非公開

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 119 | 記事ビューア画面 | 主画面 | ヘルプ記事の表示 |

## 機能種別

公開API / HTMLレンダリング / 検索

## 入力仕様

### 入力パラメータ

#### ポータル表示

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| slug | String | Yes | ポータルのslug | Portal存在確認、archived=false |
| locale | String | No | ロケール | ポータルのallowed_locales |

#### 記事一覧

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| slug | String | Yes | ポータルのslug | Portal存在確認 |
| locale | String | No | ロケール | - |
| query | String | No | 検索クエリ | - |
| sort | String | No | ソート順（views/position） | default: position |
| page | Integer | No | ページ番号 | - |
| per_page | Integer | No | 1ページあたりの件数 | max: 100, default: 25 |

#### 記事詳細

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| slug | String | Yes | ポータルのslug | Portal存在確認 |
| article_slug | String | Yes | 記事のslug | Article存在確認 |

### 入力データソース

- URL パラメータ
- クエリパラメータ

## 出力仕様

### 出力データ

#### ポータル（HTML/JSON）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @portal | Portal | ポータル情報 |
| @locale | String | 現在のロケール |
| @og_image_url | String | OGP画像URL |

#### 記事一覧（HTML/JSON）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @articles | Array | 記事一覧（publishedのみ） |
| @articles_count | Integer | 総記事数 |

#### 記事詳細（HTML）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| @article | Article | 記事情報 |
| @parsed_content | String | Markdownレンダリング済みコンテンツ |
| @og_image_url | String | OGP画像URL |

#### トラッキングピクセル

| 項目名 | 型 | 説明 |
|--------|-----|------|
| (バイナリ) | PNG | 1x1透明ピクセル画像 |

### 出力先

- HTMLレンダリング（ブラウザ表示）
- JSON API レスポンス
- PNG画像（トラッキングピクセル）
- XML（サイトマップ）

## 処理フロー

### 処理シーケンス

```
1. ポータル表示
   └─ リクエスト受信 → ポータル取得 → ロケール確認 → リダイレクトまたは表示

2. 記事一覧
   └─ リクエスト受信 → ポータル取得 → 公開記事フィルタ → 検索・ソート → ページネーション → 表示

3. 記事詳細
   └─ リクエスト受信 → ポータル・記事取得 → Markdownレンダリング → 表示

4. トラッキングピクセル
   └─ リクエスト受信 → 記事取得 → 閲覧数インクリメント → 1x1 PNG返却
```

### フローチャート

```mermaid
flowchart TD
    A[公開APIリクエスト] --> B{エンドポイント}
    B -->|/hc/:slug| C[ポータル表示]
    B -->|/hc/:slug/:locale/articles| D[記事一覧]
    B -->|/hc/:slug/articles/:article_slug| E[記事詳細]
    B -->|/hc/:slug/articles/:article_slug.png| F[トラッキングピクセル]
    B -->|/hc/:slug/sitemap.xml| G[サイトマップ]

    C --> H{locale指定?}
    H -->|No| I[デフォルトlocaleにリダイレクト]
    H -->|Yes| J[ポータル表示]

    D --> K[published記事フィルタ]
    K --> L{query指定?}
    L -->|Yes| M[全文検索]
    L -->|No| N[ソート・ページネーション]
    M --> N
    N --> O[一覧表示]

    E --> P[記事取得]
    P --> Q[Markdownレンダリング]
    Q --> R[詳細表示]

    F --> S[記事取得]
    S --> T{published?}
    T -->|Yes| U[views++]
    T -->|No| V[何もしない]
    U --> W[1x1 PNG返却]
    V --> W
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-159-01 | アーカイブ除外 | archived=trueのポータルは表示されない | 全エンドポイント |
| BR-159-02 | 公開記事のみ | status=publishedの記事のみ表示 | 記事一覧・詳細 |
| BR-159-03 | デフォルトlocaleリダイレクト | locale未指定時はポータルのデフォルトlocaleにリダイレクト | ポータル表示 |
| BR-159-04 | 閲覧数追跡 | トラッキングピクセルアクセス時にviews+1 | トラッキングピクセル |
| BR-159-05 | per_page上限 | 1ページあたりの最大件数は100 | 記事一覧 |
| BR-159-06 | iframeアクセス許可 | show_plain_layout=trueの場合、X-Frame-Options削除 | 全エンドポイント |

### 計算ロジック

```ruby
# per_pageの正規化
per_page = [list_params[:per_page].to_i, 100].min
per_page = 25 if per_page < 1

# ソート
if list_params[:sort] == 'views'
  @articles.order_by_views
else
  @articles.order_by_position
end
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| show | portals | SELECT | ポータル取得 |
| index (articles) | articles | SELECT | 記事一覧取得 |
| show (article) | articles | SELECT | 記事詳細取得 |
| tracking_pixel | articles | UPDATE | 閲覧数インクリメント |

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

#### portals

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | - | slug, archived=false | ポータル取得条件 |

#### articles

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | - | status=published | 公開記事のみ |
| UPDATE | views | views + 1 | update_column使用 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | NotFound | ポータルが存在しないまたはアーカイブ済み | 正しいslugを指定 |
| 404 | NotFound | 記事が存在しない | 正しいarticle_slugを指定 |
| 404 | NotFound | カテゴリが存在しない | 正しいcategory_slugを指定 |

### リトライ仕様

特になし

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

- views更新はupdate_columnで単一UPDATE
- トランザクション不要

## パフォーマンス要件

- ポータル表示：200ms以内
- 記事一覧：300ms以内（検索含む）
- トラッキングピクセル：50ms以内（24時間キャッシュ）

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

- 公開アクセスのため認証不要
- アーカイブポータルへのアクセス拒否
- X-Frame-Optionsのオプショナル削除（iframe埋め込み用）
- XSS防止：Markdownレンダリング時のサニタイズ

## 備考

- `portal`レイアウトを使用してヘルプセンター専用のUIを提供
- Enterprise版では記事にカスタム機能が追加される可能性あり

---

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

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

### 推奨読解順序

#### Step 1: ベースコントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | base_controller.rb | `app/controllers/public/api/v1/portals/base_controller.rb` | 共通処理 |

**主要処理フロー**:
- **4-8行目**: before_action - レイアウト、テーマ、グローバル設定
- **12-13行目**: show_plain_layout - iframe用オプション
- **24-48行目**: set_locale - ロケール切り替え
- **51-53行目**: allow_iframe_requests - X-Frame-Options削除

#### Step 2: ポータルコントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | portals_controller.rb | `app/controllers/public/api/v1/portals_controller.rb` | ポータル表示・サイトマップ |

**主要処理フロー**:
- **7-9行目**: show - ポータル表示
- **11-15行目**: sitemap - サイトマップ生成
- **19-22行目**: portal - ポータル取得（archived=false）
- **24-27行目**: redirect_to_portal_with_locale - デフォルトlocaleリダイレクト

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | articles_controller.rb | `app/controllers/public/api/v1/portals/articles_controller.rb` | 記事一覧・詳細・トラッキング |

**主要処理フロー**:
- **8-18行目**: index - 記事一覧（published、検索、ソート、ページネーション）
- **20-22行目**: show - 記事詳細
- **24-37行目**: tracking_pixel - 閲覧数追跡、1x1 PNG返却

#### Step 4: カテゴリコントローラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | categories_controller.rb | `app/controllers/public/api/v1/portals/categories_controller.rb` | カテゴリ一覧・詳細 |

**主要処理フロー**:
- **7-9行目**: index - カテゴリ一覧（position順）
- **11-13行目**: show - カテゴリ詳細

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

```
Public::Api::V1::PortalsController
    │
    ├─ before_action :ensure_custom_domain_request
    ├─ before_action :portal
    │      └─ Portal.find_by!(slug:, archived: false)
    │
    ├─ show
    │      └─ redirect_to_portal_with_locale (if locale.blank?)
    │
    └─ sitemap
           └─ @help_center_url = custom_domain || ChatwootApp.help_center_root

Public::Api::V1::Portals::ArticlesController
    │
    ├─ before_action :portal
    ├─ before_action :set_article (show)
    │
    ├─ index
    │      ├─ @portal.articles.published
    │      ├─ search_articles
    │      ├─ order_by_sort_param
    │      └─ limit_results
    │
    ├─ show
    │      ├─ set_article
    │      └─ render_article_content (Markdown)
    │
    └─ tracking_pixel
           ├─ find_by(slug:)
           ├─ increment_view_count (if published?)
           └─ send_file tracking-pixel.png
```

### データフロー図

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

GET /hc/:slug       →   PortalsController#show      →   HTML (portal layout)
                           │
                           └─ Portal.find_by!(archived: false)

GET /hc/:slug/:locale/articles
                    →   ArticlesController#index    →   HTML (articles list)
                           │
                           ├─ articles.published
                           ├─ text_search (if query)
                           └─ order + paginate

GET /hc/:slug/articles/:article_slug
                    →   ArticlesController#show     →   HTML (article detail)
                           │
                           └─ ChatwootMarkdownRenderer

GET /hc/:slug/articles/:article_slug.png
                    →   ArticlesController#tracking_pixel  →   1x1 PNG
                           │
                           └─ increment_view_count
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| base_controller.rb | `app/controllers/public/api/v1/portals/base_controller.rb` | コントローラー | 共通処理 |
| portals_controller.rb | `app/controllers/public/api/v1/portals_controller.rb` | コントローラー | ポータル表示・サイトマップ |
| articles_controller.rb | `app/controllers/public/api/v1/portals/articles_controller.rb` | コントローラー | 記事一覧・詳細・トラッキング |
| categories_controller.rb | `app/controllers/public/api/v1/portals/categories_controller.rb` | コントローラー | カテゴリ一覧・詳細 |
| chatwoot_markdown_renderer.rb | `app/services/chatwoot_markdown_renderer.rb` | サービス | Markdownレンダリング |
| portal.html.erb | `app/views/layouts/portal.html.erb` | ビュー | ヘルプセンターレイアウト |
| routes.rb | `config/routes.rb` | 設定 | 公開APIルーティング |
