# Cloudflare Image Resizing 運用

公開画像のリサイズ / フォーマット変換 / 品質調整を Cloudflare Workers の `cf.image` で行う仕組み。

## 全体構成

```
client (web home/portal)
  └─ <img src="/cdn/img?url=...&w=400&q=80&f=webp">
        │
        └─ Cloudflare Workers (parky-api)
              cdn-image.ts:cdnImageRoutes
              ├─ allowlist 検証 (SSRF 対策)
              ├─ fetch(origin, { cf: { image: {...} } })
              ├─ Workers Cache API (1d edge cache + 30d swr)
              └─ 失敗時は origin URL へ 302 fallback
```

- **Cloudflare Pro / Business / Enterprise plan** で `cf.image` が有効化される。Free plan では fallback により原寸返却で動く。
- 画像本体の保管は R2 / Supabase Storage（既存）。Image Resizing は変換層のみ。
- Cloudflare Images Platform（uploads + variants）は採用しない。Image Resizing + R2 で十分カバーできるため。

## API
[parky/api/src/bff/web/cdn-image.ts](../../api/src/bff/web/cdn-image.ts)

| Param | 範囲 | 既定 | 役割 |
|---|---|---|---|
| `url` | 必須、絶対 URL、allowlist 内 | — | 元画像 |
| `w` | 32-4096 | — | 幅 |
| `h` | 32-4096 | — | 高さ |
| `q` | 1-100 | 80 | 品質 |
| `f` | webp / avif / auto / json | auto | 出力フォーマット |
| `fit` | scale-down / cover / contain / crop | scale-down | リサイズ動作 |

### Allowlist
[buildAllowedHostSuffixes()](../../api/src/bff/web/cdn-image.ts#L49) で動的に組み立てる。
- `R2_PUBLIC_BASE` の host (例: cdn.parky.co.jp)
- `SUPABASE_URL` の host
- 固定: `.r2.dev`, `.supabase.co`, `cdn.parky.co.jp`, `assets.parky.co.jp`

外部画像 (例: 旧 WP の任意画像) は変換できない。直リンクで使う。

## クライアント helper

### Vite portals (admin / owner / marketing)
[web/packages/ui/src/cdn-image.ts](../../web/packages/ui/src/cdn-image.ts) — `@parky/ui` から re-export。

```tsx
import { cdnImageUrl, cdnImageSrcSet, CDN_IMAGE_PRESETS } from '@parky/ui';

<img
  src={cdnImageUrl(spot.cover_url, CDN_IMAGE_PRESETS.card)}
  srcSet={cdnImageSrcSet(spot.cover_url, [400, 800, 1200])}
  sizes="(max-width: 768px) 100vw, 50vw"
  alt={spot.name}
/>
```

| Preset | width | format | 用途 |
|---|---|---|---|
| `thumb` | 200x200 cover | webp q75 | アバター・小カード |
| `card` | 400 | webp q80 | リスト・記事一覧 |
| `hero` | 1200 | webp q80 | LP・detail トップ |
| `og` | 1200x630 cover | webp q85 | OGP / SNS 共有 |
| `detail` | 1600 | webp q85 | 拡大ビュー |

### Astro home
[web/home/src/lib/image-utils.ts](../../web/home/src/lib/image-utils.ts) — Supabase Storage Transform と CDN proxy の両方を扱う独自 helper。home の Astro pages はこちらを使う（記事画像が Supabase Storage 由来のため）。

`<DynamicImage>` Astro コンポーネントが共通入口で、`toOptimizedUrl()` 経由で 2 系統を自動選択。

### 住み分け

| 場所 | helper | 元画像の出所 |
|---|---|---|
| `web/portal/admin` `web/portal/owner` `web/portal/marketing` | `@parky/ui` `cdnImageUrl` | R2 cdn.parky.co.jp（管理画面アップロード） |
| `web/home` (Astro) | `web/home/src/lib/image-utils.ts` `toOptimizedUrl` | Supabase Storage（記事添付） + R2 |

## Cache 動作

- Edge: 1 日 (`Cache-Control: public, max-age=86400, s-maxage=86400, stale-while-revalidate=2592000`)
- Worker Cache API: `caches.default.put` で URL ベース cache key
- 同 URL + 同 query は次回 HIT（`x-cdn-cache: HIT` ヘッダ）

## トラブルシュート

### 全部 fallback (302) になる
- Cloudflare account が Free plan のまま → Pro 以上に upgrade 必要
- Image Resizing が account dashboard で有効化されていない → ON にする
- `cf-image-error` レスポンスヘッダが原因。ログ確認: `wrangler tail --format json | jq 'select(.message[0]=="cf-image-error")'`

### CORS エラー
- proxy レスポンスは `Access-Control-Allow-Origin: *` を強制付与済み。
- それでも失敗する場合は client 側で `crossorigin="anonymous"` を `<img>` に付ける。

### ハッシュ不一致 (SRI 連動)
- proxy の URL は SRI 対象外（query 違いで内容変動するため）。
- `<img>` 自体は SRI なし、`<link rel="preload">` で integrity を付ける場合は固定 URL のみ対応する。
