# 機能設計書 48-oEmbed

## 概要

本ドキュメントは、Ghost CMSにおけるoEmbed機能の設計仕様を記載したものである。

### 本機能の処理概要

oEmbed機能は、外部コンテンツ（YouTube動画、Twitter投稿、各種Webページ等）をGhostの記事内に埋め込むための機能である。URLを指定するだけで、適切な埋め込みHTMLやメタデータを自動取得し、リッチなコンテンツ表示を実現する。oEmbedプロトコルをサポートするプロバイダーに加え、ブックマークカード形式でのメタデータ取得にも対応する。

**業務上の目的・背景**：現代のコンテンツは様々なプラットフォームに散在しており、それらを記事内に統合して表示するニーズが高い。oEmbed機能により、YouTube、Twitter、Instagram等の主要プラットフォームのコンテンツを簡単に埋め込み可能にし、コンテンツの魅力を高める。

**機能の利用シーン**：
- 記事エディタでのYouTube動画の埋め込み
- Twitter投稿の引用表示
- 外部サイトへのブックマークカード作成
- Spotifyプレイリストの埋め込み
- GitHubリポジトリのリンクカード表示

**主要な処理内容**：
1. URL解析：指定されたURLのプロバイダーを判定
2. oEmbedプロバイダー検索：既知のoEmbedプロバイダーリストとマッチング
3. oEmbedデータ取得：プロバイダーのoEmbedエンドポイントからデータ取得
4. ブックマークフォールバック：oEmbed非対応の場合はメタデータをスクレイピング
5. 画像処理：サムネイル・アイコンのダウンロードと保存
6. レスポンス生成：埋め込みHTML、メタデータを返却

**関連システム・外部連携**：
- @extractus/oembed-extractor（oEmbedプロバイダー判定）
- metascraper（Webページメタデータ抽出）
- 外部oEmbedプロバイダー（YouTube、Twitter等）

**権限による制御**：
- 認証不要（permissions: false）
- URLは外部フェッチの対象となるためSSRF対策が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 13 | エディタ画面 | 補助機能 | 記事内への外部コンテンツ埋め込み |

## 機能種別

データ取得 / 外部API連携 / Webスクレイピング

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| url | string | Yes | 埋め込み対象のURL | 有効なURL形式 |
| type | string | No | カード種別（bookmark/mention） | bookmark, mention, または未指定 |

### 入力データソース

- Admin API（GET /ghost/api/admin/oembed/）
- エディタからのURLペースト

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| version | string | oEmbedバージョン（通常"1.0"） |
| type | string | コンテンツ種別（video/photo/rich/bookmark） |
| html | string | 埋め込みHTML（video/rich） |
| url | string | コンテンツURL（photo） |
| title | string | タイトル |
| width | integer | 幅 |
| height | integer | 高さ |
| author_name | string | 著者名 |
| author_url | string | 著者URL |
| provider_name | string | プロバイダー名 |
| provider_url | string | プロバイダーURL |
| thumbnail_url | string | サムネイルURL |
| metadata | object | ブックマークメタデータ（bookmark type） |

### 出力先

- APIレスポンス（JSON形式）
- エディタ内埋め込みプレビュー

## 処理フロー

### 処理シーケンス

```
1. URL受信
   └─ GET /ghost/api/admin/oembed/?url={url}&type={type}
2. カスタムプロバイダーチェック
   └─ 登録済みカスタムプロバイダーで処理可能か確認
3. 既知プロバイダーチェック
   └─ @extractus/oembed-extractorでプロバイダー判定
4. プロバイダー分岐
   ├─ 既知プロバイダー: oEmbedエンドポイントからデータ取得
   └─ 未知プロバイダー: ページHTMLをフェッチ
5. oEmbedデータ抽出
   ├─ <link rel="alternate" type="application/json+oembed">を検索
   └─ 見つからない場合: ブックマークデータにフォールバック
6. ブックマーク処理（該当時）
   ├─ metascraperでメタデータ抽出
   ├─ アイコン画像のダウンロード・保存
   └─ サムネイル画像のダウンロード・保存
7. レスポンス返却
   └─ JSON形式でoEmbed/ブックマークデータを返却
```

### フローチャート

```mermaid
flowchart TD
    A[URL受信] --> B{カスタムプロバイダー?}
    B -->|Yes| C[カスタムプロバイダーで処理]
    B -->|No| D{type=bookmark?}
    D -->|Yes| E[ページHTML取得]
    D -->|No| F{既知プロバイダー?}
    F -->|Yes| G[oEmbedデータ取得]
    F -->|No| E
    E --> H{oEmbedリンク発見?}
    H -->|Yes| I[oEmbedデータ取得]
    H -->|No| J[ブックマークデータ生成]
    G --> K[レスポンス返却]
    I --> K
    J --> L[画像処理]
    L --> K
    C --> K
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-48-001 | WordPress除外 | wp-json/oembedはブックマードにフォールバック | WordPress oEmbed検出時 |
| BR-48-002 | linkタイプ除外 | oEmbedタイプがlinkの場合はブックマークにフォールバック | type=link |
| BR-48-003 | 必須フィールド | photo/video/richには必須フィールドあり | 各タイプ |
| BR-48-004 | 画像保存 | ブックマークのアイコン・サムネイルはローカル保存 | ブックマーク生成時 |
| BR-48-005 | YouTubeライブ変換 | YouTubeの/live/URLを/watch?v=形式に変換 | YouTube live URL |

### 計算ロジック

既知プロバイダー判定:
```javascript
const findUrlWithProvider = (url) => {
    const {hasProvider} = require('@extractus/oembed-extractor');

    // URLバリエーションを生成してテスト
    let baseUrl = url.replace(/^\/\/|^https?:\/\/(?:www\.)?/, '');
    let testUrls = [
        `https://${baseUrl}`,
        `https://www.${baseUrl}`,
        `http://${baseUrl}`,
        `http://www.${baseUrl}`
    ];

    for (let testUrl of testUrls) {
        if (hasProvider(testUrl)) {
            return {url: testUrl, provider: true};
        }
    }
    return {url, provider: false};
};
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 画像保存 | (ファイルシステム) | WRITE | アイコン・サムネイル画像の保存 |

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

oEmbedはデータベース操作なし。画像はファイルシステムに保存。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 422 | ValidationError | URLが空 | 有効なURLを指定 |
| 422 | ValidationError | プロバイダー不明、メタデータ不足 | サポートされているURLを使用 |
| 422 | ValidationError | 認証エラー（401/403） | 公開コンテンツのURLを使用 |

### リトライ仕様

- 外部フェッチ失敗時はエラーをログ出力
- リトライは実装されていない（即座にエラー返却）

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

データベース操作がないため、トランザクション管理は不要。

## パフォーマンス要件

- oEmbedデータ取得: 2000ms（外部フェッチタイムアウト）
- ブックマークデータ取得: 外部サイト応答依存

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

- SSRF対策: @tryghost/requestを使用（native fetchは使用禁止）
- User-Agentを設定してbot検出を回避
- プライベートリソースへのアクセスは401/403で拒否
- URLの正規化とバリデーション

## 備考

- カスタムプロバイダー登録でTwitter、NFT等の特殊対応が可能
- 画像処理失敗時はデフォルトアイコンを使用
- 開発環境ではURL検証を緩和

---

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

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

### 推奨読解順序

#### Step 1: APIエンドポイントを理解する

まず、oEmbed APIのエントリーポイントを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | oembed.js | `ghost/core/core/server/api/endpoints/oembed.js` | APIエンドポイント |

**読解のコツ**: シンプルな構造で、権限なし（permissions: false）でデータ取得のみ行う。

**主要処理フロー**:
- **7-8行目**: permissions: falseで認証不要
- **12-15行目**: dataでurl, typeを受け取り
- **17-20行目**: oembed.fetchOembedDataFromUrlを呼び出し

#### Step 2: oEmbedサービスを理解する

oEmbedのコアロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | oembed-service.js | `ghost/core/core/server/services/oembed/oembed-service.js` | oEmbedサービス実装 |

**主要処理フロー**:
1. **27-51行目**: findUrlWithProviderで既知プロバイダー判定
2. **74-91行目**: OEmbedServiceクラスのコンストラクタ
3. **96-98行目**: registerProviderでカスタムプロバイダー登録
4. **103-108行目**: unknownProviderでエラースロー
5. **113-131行目**: knownProviderでoEmbedデータ抽出
6. **265-358行目**: fetchBookmarkDataでメタデータスクレイピング
7. **441-550行目**: fetchOembedDataFromUrlでメイン処理

#### Step 3: プロバイダー判定を理解する

既知プロバイダーの判定ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | oembed-service.js | `ghost/core/core/server/services/oembed/oembed-service.js` | findUrlWithProvider関数 |

**主要処理フロー**:
- **27-51行目**: @extractus/oembed-extractorのhasProviderを使用
- **34-40行目**: URLバリエーション（http/https、www有無）を生成してテスト
- **42-48行目**: マッチしたらprovider: trueを返却

#### Step 4: ブックマークデータ生成を理解する

metascraperを使用したメタデータ抽出を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | oembed-service.js | `ghost/core/core/server/services/oembed/oembed-service.js` | fetchBookmarkData関数 |

**主要処理フロー**:
- **283-295行目**: metascraperの設定（url, title, description等）
- **300-306行目**: metascraper実行
- **312-319行目**: メタデータをマッピング
- **327-350行目**: アイコン・サムネイル画像の処理

#### Step 5: oEmbedデータ抽出を理解する

HTMLからoEmbedリンクを抽出する処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | oembed-service.js | `ghost/core/core/server/services/oembed/oembed-service.js` | fetchOembedData関数 |

**主要処理フロー**:
- **367-377行目**: cheerioでoEmbedリンクを検索
- **379-384行目**: WordPress oEmbedはスキップ
- **386-388行目**: oEmbedエンドポイントからJSONを取得
- **396-411行目**: 既知フィールドのみ抽出

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

```
API Request (GET /ghost/api/admin/oembed/)
    │
    ├─ oembed.js (controller)
    │      └─ oembed.fetchOembedDataFromUrl(url, type)
    │
    └─ oembed-service.js (OEmbedService)
           │
           ├─ カスタムプロバイダーチェック
           │      └─ provider.canSupportRequest(urlObject)
           │             └─ provider.getOEmbedData(urlObject, externalRequest)
           │
           ├─ findUrlWithProvider(url)
           │      └─ @extractus/oembed-extractor.hasProvider()
           │
           ├─ knownProvider(url)
           │      └─ @extractus/oembed-extractor.extract()
           │
           ├─ fetchPageHtml(url)
           │      └─ externalRequest() - 外部HTTPフェッチ
           │
           ├─ fetchOembedData(url, html)
           │      ├─ cheerio - HTMLパース
           │      └─ fetchPageJson(oembedUrl)
           │
           └─ fetchBookmarkData(url, html, type)
                  ├─ metascraper - メタデータ抽出
                  └─ processImageFromUrl() - 画像保存
```

### データフロー図

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

url ────────────────────▶ ┌──────────────────────────┐
type ───────────────────▶ │   oembed-service.js      │
                          │          │               │
                          │          ▼               │
                          │   findUrlWithProvider()  │
                          │          │               │
                          ├──────────┼───────────────┤
                          │   既知   │   未知         │
                          │   ▼      │   ▼           │
                          │ extract() │ fetchPageHtml()│
                          │          │   │           │
                          │          │   ▼           │
                          │          │ fetchOembedData()│
                          │          │   │           │
                          │          │   ▼           │
                          │          │ fetchBookmarkData()│
                          └──────────┴───────────────┘
                                       │
                                       ▼
                          ┌──────────────────────────┐
                          │  JSON Response           │
                          │  {                       │
                          │    type: "video/bookmark",│
                          │    html: "...",          │
                          │    metadata: {...}       │
                          │  }                       │
                          └──────────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| oembed.js | `ghost/core/core/server/api/endpoints/oembed.js` | ソース | APIエンドポイント |
| oembed-service.js | `ghost/core/core/server/services/oembed/oembed-service.js` | ソース | oEmbedサービス |
| service.js | `ghost/core/core/server/services/oembed/service.js` | ソース | サービスインスタンス生成 |
| index.js | `ghost/core/core/server/services/oembed/index.js` | ソース | サービスエクスポート |
| twitter-oembed-provider.js | `ghost/core/core/server/services/oembed/twitter-oembed-provider.js` | ソース | Twitterカスタムプロバイダー |
| nft-oembed-provider.js | `ghost/core/core/server/services/oembed/nft-oembed-provider.js` | ソース | NFTカスタムプロバイダー |
