# CLAUDE.md

セットアップとデプロイのコマンドは README.md を参照。

## 日付の扱い

Netlify のビルドは **UTC** で走る。事前レンダリングした結果と閲覧者のブラウザでの
再計算が食い違うとハイドレーションが壊れるため、日付の判定・表示は必ず
`lib/jst.js` を通し、基準時刻は `new Date()` ではなく `build_info.updated`
（ビルド時刻）を使う。

この不具合はローカル（JST でビルド → JST で表示）では再現しない。検証は
`TZ=UTC pnpm build:skip` でビルドしてから、`.output/public` を配信して
**全ページをブラウザで開く**こと（トップだけ見て `/new/` を見落とした前例がある）。

日付ライブラリは dayjs。ロケールは `lib/jst.js` で `ja` に決めている。
Vite は使うものだけを読むため、ここを通さないと曜日が英語になる。
ロケールは副作用だけの import（`import 'dayjs/locale/ja'`）では効かず、
値として読んで `dayjs.locale(ja)` に渡す必要がある。

`scripts/` 側（フィードの取得）は素の `Date` で日付を扱う。RSS の pubDate は
`scripts/parse-pub-date.js`、収録時間の計算は `lib/format-seconds.js`。

## 音声の再生

音声は配信元の enclosure URL をブラウザから直接再生している
（`components/episode-player.vue` の `preparePlayer`）。配信者側の計測は配信
サーバーのログを IP と UA で数えるので、この形なら再生がそのまま各番組の
統計になる。次のことをすると、その数字を奪ってしまうのでしない。

- 音声を中継する・キャッシュする（配信元のログが Netlify の IP で埋まる）
- enclosure URL を「正規化」してプレフィックスやクエリを落とす
  （Podtrac などの計測が効かなくなる。`scripts/pf-util.js` の `audioUrl`）
- ファイルの保存（ダウンロード）機能を付ける
- 再生時の UA を偽装する。ブラウザからは変えられないし、変えるべきでもない
  （フィード取得側の UA は `scripts/wget-with-timeout.js` で名乗っている）
- `referrer` の meta を足す。既定のままなら配信元へ podcastfreaks.com が伝わる

フィードに `itunes:block` / `podcast:block` が指定された番組は `scripts/fetch-feeds.js` が
一覧から外し、`/errors` に「掲載を止めている番組」として出す。

## 生成物とデータ

- `static/downloads/`（RSS・カバー画像・`build_info.json`・`episodes/`）は
  `pnpm fetch-feeds` が生成する。gitignore 対象。ページは `build_info.json` を
  ビルド時に import するので、無いと中身のないサイトになる
- エピソードは `build_info.json` ではなく `episodes/<key>.json` に番組ごとに
  置く。一覧で行を開いたときにしか使わないのに、`build_info.json` 全体の9割を
  占めていたため。一覧は行を開いた時点でその番組のぶんだけ読む
  （`pages/index.vue` の `loadEpisodes`）ので、`build_info.json` に何かを
  足すときは初期表示に載るかどうかを見る
- エピソードは1番組で1300話を超えることがある。一覧は30話ずつ描き、
  音声は再生を押すまで作らない（`components/episode-player.vue` の
  `preparePlayer`）。長さはフィードの `itunes:duration` から出している
- `pnpm build` は `fetch-feeds`（フィードとカバー画像の取得）から走る。取得済みの
  ものを使うなら `pnpm build:skip`。pnpm は npm のライフサイクル（`pre` の付く
  スクリプト）を既定で実行しないので、`build` の中で明示的に呼んでいる
- `data/added-at.json`（番組の登録日）と `data/apple-podcasts.json`（Apple Podcasts の
  リンク）は GitHub Actions が生成する。手で編集するのは後者の `"source": "manual"`
  の項目だけ
- フィードのエラーや警告への対処は `.claude/skills/feed-triage/` にまとめてある
- 番組を新しく登録する手順は `.claude/skills/add-channel/` にまとめてある

## テスト

`lib/` と `scripts/` の素の関数を Vitest で固めてある。

```sh
pnpm test        # 一度だけ走らせる
pnpm test:watch  # 直しながら見る
```

見ているのは、壊れると影響が大きくて、目で確かめるのが面倒なところ。

- `lib/compare.js` … 一覧の並べ替え。vue-tables-2 から自前の table へ移した
  ときに、234行の並びが1つも変わらないことを確かめながら移植した部分。
  同じ値でも 0 を返さない（Array#sort の作法から外れる）挙動も、
  そのままであることを確かめている
- `lib/jst.js` … 日本時間への固定。テストは `TZ=UTC` で走る
  （`vitest.config.js`）。手元が JST でも気づけるように
- `scripts/parse-pub-date.js` … RSS の pubDate。GMT のような名前の
  タイムゾーン、実際と食い違う曜日など、過去に踏んだ形をそのまま置いてある
- `scripts/pf-util.js` … 収録時間と更新頻度の集計。moment を外したときに、
  配信中の234フィードで旧実装と突き合わせながら書き換えた部分

画面の描画（一覧の絞り込みや子行の開け閉め）はここでは見ていない。
そちらを変えたときは、ブラウザで実際に動かして確かめる。

## スタイル

スタイルはプレーン CSS のネスト記法で書く（`.vue` の `<style>` と
`assets/common.css`）。Sass は使っていない。

ネストは Vite がビルド時に平坦なセレクタへ展開する（Nuxt 2 のときは
`postcss-preset-env` の `nesting-rules` を有効にしていた）。書くときの注意が3つ。

- ネストの中で**要素名から始まるセレクタには `&` を付ける**（`& svg`）。
  付けないとプロパティ名と解釈されて壊れる
- **`@media` の直下では `&` を付けない**。そこでの `&` は `:root` を指すので、
  `:root div.row` という別物のセレクタになる
- **`>` で始まるセレクタには要素かクラスを書く**（`>.badge`、`& > table`）。
  `>*` や `>:first-child` のように書くと、Vue 3 の scoped 変換が
  `[data-v-x]` を別の位置に差し込み、`.links [data-v-x]>:not(:first-child)`
  のような別物のセレクタになる（アイコンの間隔と About の凡例が崩れた）

ヘッダーなどの紫のグラデーションは、もとは Sass のミックスインだった。
`assets/common.css` の `:root` に `--brand-texture` などとして置いてある。

## 依存関係

Nuxt 4 / Vue 3 / Vite。Node は `.node-version` に固定。

パッケージマネージャは pnpm で、バージョンは `package.json` の
`packageManager` に書いてある（corepack が読む）。宣言していない依存が
見えないので、`package.json` に無いものを import していると気づける
（yarn のときは `axios` が `@nuxtjs/axios` 経由で使えてしまっていた）。

pnpm 10 以降は、入れたパッケージのビルドスクリプトを既定で走らせない。
esbuild（Vite のビルド）と sharp（カバー画像とアイコン）は必要なので、
`pnpm-workspace.yaml` の `allowBuilds` で許可している。ここに無いものが
必要になると `ERR_PNPM_IGNORED_BUILDS` で止まる。

pnpm の設定は `package.json` の `pnpm` フィールドではなく
`pnpm-workspace.yaml` に置く（pnpm 10 以降。単一のパッケージでも同じ）。

出力先は `.output/public`。`netlify.toml` の `publish` と `pnpm deploy` の
`--dir` もそこを指している。

そのために `nuxt.config.js` で Nitro のプリセットを `static` に固定している。
外すと Netlify の上だけ `netlify-static` が自動で選ばれ、手元と2つ食い違う。

- 出力先が `dist` に変わり、`netlify.toml` の `publish` と合わずデプロイが落ちる
- `_payload.json` が出力されない。事前レンダリングした HTML はこれを読む前提
  なので 404 になり、ハイドレーション後の状態が復元されない（一覧のチェックが
  全部外れ、Download OPML が押せなくなる）。**警告もエラーも出ない**

手元で Netlify と同じ条件を試すなら `NETLIFY=true pnpm build:skip` で再現できる。

## コミットメッセージをファイルで渡すとき

`git commit -F` に渡すファイルは、`$TMPDIR/msg.txt` のような汎用名にしない。
zsh は noclobber が効いていて、同名のファイルが残っていると `cat > file` が
`file exists` で失敗する。それでも後続の `git commit -F` は成功してしまうため、
**別セッションが残した古いメッセージでコミットされる**（前例あり）。

固有の名前を付けて、`>|` で上書きする。

```sh
cat >| "$TMPDIR/pf-<内容>-msg.txt" <<'MSG'
...
MSG
```
