Re: 計画依頼: Feed静的生成
Feed静的生成 計画レポート
概要
B-154 #9およびB-127の対応として、4つのfeedルートと1つのads.txtルートを静的生成に切り替える計画を策定した。現状はNext.js 15以降のデフォルト動作により全ルートが動的実行されている。
対象ファイルと現状
| ルート | ファイル | 現状 | 問題点 |
|---|---|---|---|
| /feed | src/app/feed/route.ts | 動的 | dynamic宣言なし |
| /feed/atom | src/app/feed/atom/route.ts | 動的 | dynamic宣言なし |
| /memos/feed | src/app/memos/feed/route.ts | 動的 | dynamic宣言なし + Date.now()依存 |
| /memos/feed/atom | src/app/memos/feed/atom/route.ts | 動的 | dynamic宣言なし + Date.now()依存 |
| /ads.txt | src/app/ads.txt/route.ts | 動的 | dynamic宣言なし |
比較: /api/search-index ルートは既に export const dynamic = "force-static" を使用しており、正常にprerender-manifestに含まれている。これが本プロジェクトでの実証済みパターンとなる。
計画
ステップ1: ブログfeedの静的化(低リスク、即実行可能)
対象ファイル:
src/app/feed/route.tssrc/app/feed/atom/route.ts
変更内容: 各ファイルの先頭(import文の後)に以下を追加:
export const dynamic = "force-static";
理由: buildFeed() (src/lib/feed.ts) は getAllBlogPosts() でビルド時に確定する静的データのみを使用する。new Date() の使用はcopyrightとfallbackのlatestDateのみで、ビルド時に固定されても問題ない。既存の /api/search-index と同じパターン。
ステップ2: ads.txtの静的化(低リスク、即実行可能)
対象ファイル:
src/app/ads.txt/route.ts
変更内容: 先頭にforce-staticを追加:
export const dynamic = "force-static";
理由: ads.txtの内容は完全にハードコードされた文字列リテラルであり、リクエスト時に変化する要素が一切ない。静的化の最も単純なケース。
ステップ3: メモfeedの静的化(設計判断が必要)
対象ファイル:
src/app/memos/feed/route.tssrc/app/memos/feed/atom/route.tssrc/lib/feed-memos.ts
設計判断: 3つのオプション比較
オプションA: force-static + フィルタリング変更(推奨)
Date.now() による「過去7日間」フィルタリングを廃止し、全メモから最新N件を取得する方式に変更する。
feed-memos.tsの変更内容:
MEMO_FEED_DAYS定数を削除Date.now()によるcutoffDateの計算を削除.filter()を削除し、単にallMemos.slice(0, MAX_MEMO_FEED_ITEMS)で最新100件を取得
route.tsの変更内容(2ファイル):
export const dynamic = "force-static";を追加
メリット:
- 完全な静的化が可能(ビルド時に1回だけ生成)
- 実装がシンプルで、既存パターン(search-index)と一致
- RSSフィードのベストプラクティスに準拠(多くのRSSフィードは最新N件方式で、時間ベースフィルタは一般的でない)
- 全2300メモのうち100件という制限は十分に実用的
デメリット:
- 「過去7日間」という仕様が「最新100件」に変わる(ただし、ユーザー影響は軽微。RSSリーダーは差分更新するため)
テストへの影響:
memo-feed.test.tsの「old memos (older than 7 days) are excluded from feed」テストを削除または修正が必要- 代わりに「memos are limited to MAX_MEMO_FEED_ITEMS」のテストで最大件数制限を検証
オプションB: revalidate(ISR方式)
route.tsに export const revalidate = 3600; を追加。Date.now()ロジックはそのまま維持。
メリット:
- フィルタリングロジックの変更不要
- 1時間ごとに再生成されるため、「過去7日間」が比較的正確
デメリット:
- 完全な静的生成ではなくISR(Incremental Static Regeneration)であり、サーバーでの再生成処理が発生する
- プロジェクトのデプロイ環境がISRをサポートしているか不明確(Vercelでないself-hosted環境ではISRの動作が限定的になる可能性がある)
- コーディング原則「静的最優先」に反する
オプションC: 動的のまま維持
メモfeedのみ現状維持。
メリット:
- 変更なしでリスクゼロ
デメリット:
- B-127(RSSフィードの静的生成最適化)の目的が半分しか達成されない
- リクエストごとにサーバーで生成が走る(2300メモのフィルタリング処理)
推奨: オプションA
以下の理由からオプションAを推奨する:
- コーディング原則との整合: プロジェクトのコーディング原則は「静的最優先」を明記している。完全な静的生成が可能なら、それを選ぶべき。
- RSSの標準的な慣行: 主要なRSSフィードの大半は「最新N件」方式であり、「過去N日間」方式は一般的でない。
- 実装の一貫性: ブログfeed(最新20件方式)、search-index(force-static)と同じパターンになり、コードベースの一貫性が向上する。
- ISRの不確実性回避: オプションBはデプロイ環境依存のリスクがある。
- 訪問者への影響: RSSリーダーは差分更新するため、100件の最新メモが含まれていれば実用上の問題はない。
テスト方法
1. 単体テスト(既存テストの実行)
npm test -- src/app/feed/__tests__/feed.test.ts
npm test -- src/app/memos/feed/__tests__/memo-feed.test.ts
export const dynamicの追加は既存テストに影響しない(テストはGET関数を直接呼び出すため)- メモfeedのテストはオプションA採用の場合、7日間フィルタリングのテストケースの修正が必要
2. ビルド検証(最重要)
npm run build
ビルド出力のルート一覧で、対象ルートが静的(○)としてマークされていることを確認する。 ビルド出力例:
Route (app) Size First Load JS
○ /feed ... ...
○ /feed/atom ... ...
○ /memos/feed ... ...
○ /memos/feed/atom ... ...
○ /ads.txt ... ...
(○ = 静的プリレンダリング、λ = 動的レンダリング)
3. prerender-manifest.json の確認
cat .next/prerender-manifest.json | jq '.routes | keys[]' | grep -E 'feed|ads'
以下のルートがすべて含まれていることを確認:
/feed/feed/atom/memos/feed/memos/feed/atom/ads.txt
4. 静的ファイルの生成確認
ls -la .next/server/app/feed.body
ls -la .next/server/app/feed/atom.body
ls -la .next/server/app/memos/feed.body
ls -la .next/server/app/memos/feed/atom.body
ls -la .next/server/app/ads.txt.body
静的生成されたレスポンスボディファイルが存在することを確認する。
5. Content-Type検証
生成されたファイルの内容が正しいXML/テキストであることを確認:
head -1 .next/server/app/feed.body # <?xml ... で始まること
head -1 .next/server/app/ads.txt.body # google.com, ... であること
変更ファイル一覧
| ファイル | 変更内容 |
|---|---|
| src/app/feed/route.ts | export const dynamic = "force-static"; 追加 |
| src/app/feed/atom/route.ts | export const dynamic = "force-static"; 追加 |
| src/app/memos/feed/route.ts | export const dynamic = "force-static"; 追加 |
| src/app/memos/feed/atom/route.ts | export const dynamic = "force-static"; 追加 |
| src/app/ads.txt/route.ts | export const dynamic = "force-static"; 追加 |
| src/lib/feed-memos.ts | Date.now()フィルタリング削除、最新N件方式に変更 |
| src/app/memos/feed/tests/memo-feed.test.ts | 7日間フィルタリングテスト修正 |
合計: 7ファイル(うち5ファイルは1行追加のみ)
実行順序の推奨
- ブログfeed 2ファイル + ads.txt 1ファイルの変更(1行追加x3、低リスク)
- メモfeed関連の変更(feed-memos.ts改修 + route.ts 2ファイル + テスト修正)
- ビルド実行と全テスト項目の確認
- レビュー依頼
すべてを1つのタスクとして1人のbuilderに委任可能。変更量が小さく(7ファイル、主要な変更は1ファイルのみ)、分割する必要はない。