# コードレビュー依頼：pixelstomotors.com の古い住所の 301 転送

対象：Cloudflare Workers の worker/index.js（差分を下に貼る）。サイトは Astro で作った静的ファイルを Workers の静的アセット（env.ASSETS）から返している。wrangler.jsonc の run_worker_first は ["/*", "!/_astro/*", "!/site/*", "!/og/*", "!/thumbs/*", "!/embeds/*", "!/sim/*", "!/favicon*"] なので、ページへのリクエストは必ず worker の fetch() を通る。

## やろうとしていること

検索エンジン・AI のクローラーが古い住所で読みに来て 404 になっているもののうち、行き先がはっきりしているものだけを 301 で今のページへ転送する。

| 来た住所 | 転送先 | 理由 |
|---|---|---|
| /manga と /manga/ | /manga-archive | 4コマの一覧は /manga-archive。/manga という一覧ページはない（/manga/ep01 などの各話はある） |
| /manga/ep9 のような1桁 | /manga/ep09 | 話数の住所は2桁。クローラーが1桁で来ている |
| /english-4-koma/ep9 のような1桁 | /english-4-koma/ep09 | 同上（英語版） |
| /record/precision-is-not-learning | /record/precision-is-not-learning-so101 | 記事の住所の末尾が変わっていた |
| /prototype1〜6（末尾の / もあり） | /series1〜6 | 前はシリーズのページの住所が /prototypeN だった（サイトの持ち主が確認済み） |

転送しないもの（404 のまま）：/ai-agent（今は使っていないページ）、記事に載せたコードの文字列を住所と勘違いしたもの（/files/LOG395/{c} など）。

## 差分

```diff
diff --git a/worker/index.js b/worker/index.js
index 2c44687..df03666 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -26,6 +26,13 @@ export default {
     if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/media/') || url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) {
       return secured(await route(request, env, url));
     }
+    // 古い住所・まちがった住所への訪問を、今のページへ恒久転送する（2026-10-30。クローラーの「返せなかったページ」から）
+    const moved = movedTo(url.pathname);
+    if (moved) {
+      const to = new URL(moved, url);
+      to.search = url.search;
+      return Response.redirect(to.toString(), 301);
+    }
     const res = await env.ASSETS.fetch(request);
     // 検索エンジン・AI のクローラーが読みに来たか（記録の失敗でページを止めない）
     try {
@@ -40,6 +47,23 @@ export default {
   },
 };
 
+// 転送先の決まった古い住所。今は使っていないページ（/ai-agent。Dify で AI エージェントを動かしていた頃のもの）は転送しない（404 のまま）
+const MOVED = {
+  '/manga': '/manga-archive',
+  '/manga/': '/manga-archive',
+  '/record/precision-is-not-learning': '/record/precision-is-not-learning-so101',
+};
+function movedTo(path) {
+  if (MOVED[path]) return MOVED[path];
+  // シリーズのページは、前は /prototype1〜6 だった（エイイチ 2026-10-30）。同じ番号の /series へ
+  const sp = path.match(/^\/prototype([1-6])\/?$/);
+  if (sp) return `/series${sp[1]}`;
+  // 4コマの話数は2桁（ep09）。1桁（ep9）で来たら0を付けた住所へ
+  const m = path.match(/^\/(manga|english-4-koma)\/ep([1-9])$/);
+  if (m) return `/${m[1]}/ep0${m[2]}`;
+  return '';
+}
+
 async function route(request, env, url) {
     // 旧 URL の形（/pixelstomotors/…）で来たら、/pixelstomotors を外した URL へ恒久転送する
     if (url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) {
```

## 手元で確かめたこと（Node で worker を読み込み、env.ASSETS をまねて fetch を呼んだ）

- /manga → 301 /manga-archive、/manga/ → 301 /manga-archive
- /manga/ep9 → 301 /manga/ep09、/english-4-koma/ep9?x=1 → 301 /english-4-koma/ep09?x=1（クエリは残す）
- /manga/ep09・/manga/ep10・/record/precision-is-not-learning-so101・/manga-archive は転送されず、そのまま返る
- /ai-agent は転送されない
- /prototype1 → 301 /series1、/prototype6/ → 301 /series6、/prototype3?a=1 → 301 /series3?a=1、/prototype7・/prototype10 は転送されない
- 注意：1桁の話のうち、実際にページがあるのは manga が8話分・english-4-koma が6話分。ない話は転送先でも 404 になる（転送前と結果は同じ）

## レビューしてほしいこと

A1. 転送の条件（完全一致と正規表現）が、意図しない住所まで転送しないか（例：/manga/ep90、/manga-archive、/manga/ep9/ のような末尾スラッシュ）
A2. 転送のループが起きないか（転送先が再び転送されないか。html_handling は auto-trailing-slash）
A3. 転送先が存在しない 1桁の話（例 ep08 がない場合）を 301 にするのは問題か。存在を確かめてから転送すべきか
B1. 既存の動き（/api/・/media/・/pixelstomotors/ の旧形式の転送・クローラーの記録・静的アセット）を壊していないか。転送したリクエストはクローラーの記録に残らなくなるが、問題か
B2. 301（恒久）でよいか。あとで戻すことになったときの影響（ブラウザ・検索エンジンのキャッシュ）
C1. 上の表以外に、転送すべき・すべきでない住所の判断で気になる点

各項目を ✅ OK／⚠️ 要注意（何が懸念か）／❌ NG（該当行と修正案）で答え、最後に「公開してよいか」「公開前に直すべき項目」をまとめてください。
