---
title: "pixelstomotors.com は人より先に AI に読まれていた ― 次は AI から人を連れてくる"
slug: read-by-ai-before-people
date: 2026-10-30
day: 411
meta_description: "pixelstomotors.com は、人の表示が5日で130回なのに、ChatGPT の利用者の依頼で記事を読みに来たのが48回あった。AI から来た人を数える印の記録と、AI が開けなかったページの直しを入れ、トップのシリーズのカード3枚も作り直した。"
tags: [サイト改修, アクセス解析, ChatGPT, AIクローラー, utm_source, 301転送, Cloudflare Workers, Astro, codex, サムネイル]
ai_consult: true
series: "ひとり研究所の裏側"
---

**pixelstomotors.com は、人がページを見た回数が5日で130回なのに対して、ChatGPT の利用者が頼んだときに記事を読みに来た回数が48回あり、人より先に AI に読まれるサイトになり始めていました。** ただ、AI が読んだあとに人がサイトへ来ているかは、今の計測では見えていませんでした。AI の回答のリンクから来た人が「直接・不明」に混ざってしまうためです。

pixelstomotors.com は、ロボットとフィジカルAI（現実の物を動かす AI）の研究を、ひとりで記録して公開しているサイトです。記事は、人より AI が読む前提で書いています。人が調べものをするとき、検索より AI に聞くことが増えたからです。

その前提が数字のうえでどうなっているかを、サイトの計測の記録から読みました。人が見た回数・どこから来たか・どのページが読まれたか・AI と検索エンジンの巡回プログラム（クローラー）がどのページを読みに来たか、です。読んだ結果から、すぐできる直しを3つ入れました。

| やったこと | なぜ | 結果 |
|---|---|---|
| トップのシリーズのカード3枚を作り直した | 線画のイラストが続き、何の研究か伝わりにくかった | 研究室は「お手本の動きのデータ」、画像認識は「写真が処理されていく4列」、導入シミュレーションは「Unity の店」にした |
| サイトの計測の記録を読んだ | AI に読ませる準備をしてきたことが、数字に出ているかを見たかった | 人の表示は5日で130回。ChatGPT の利用者の依頼での読み取りは48回 |
| AI の回答のリンクから来た人を数えるようにした | アプリから来ると「どこから来たか」が送られず、「直接・不明」（59回で最多）に混ざる | リンクに付く印（utm_source）を記録し、「chatgpt.com（リンクの印）」と分けて出す |
| クローラーが開けなかったページを特定した | 開けなかった訪問が計62回あった | 記事のファイルの枠5つが、人が押しても開けない状態だったのを見つけて直した |
| 古い住所を今のページへ転送した | 昔のシリーズのページの住所などに、クローラーが読みに来て 404 になっていた | /prototype1〜6 → /series1〜6 など。外部の AI（codex）のレビューを通してから公開した |

計測の数字は、人と機械を見分けられるようになった 10/26 から 10/30 夕方までの、5日分です。数が少ないので、傾向として読んでいます。

【画像】LOG411_01_series_cards_after.png 作り直したあとのトップのシリーズのカード。左上はハコブくんの写真、右上はお手本の動きの線を上は白・下は黒の面に重ねた図、左下は赤い箱の写真が画素・白黒・しきい値・輪郭と4列で処理されていく図、右下は Unity で作ったファミレスの店を斜め上から見た画面


## 作業ログ ― トップのシリーズのカードを作り直した

### 最初は写真に替えて、「写真を置いただけは NG」になった

フィジカルAI研究室とサービスロボット導入シミュレーションのカードは、どちらも線画のイラストでした。これを画像に替えるよう頼まれ、まず実機の写真と Unity の画面に替えました。

- フィジカルAI研究室：LOG401 の成功した回を撮った iPhone の4K動画から、アームが赤い箱を白い箱の縁まで運んだ1コマ
- サービスロボット導入シミュレーション：Unity で作ったファミレスの店を斜め上から見た画面（背景の紺色を、導入前ガイドのカードと同じ薄いグレーに替えた）

導入シミュレーションはこれで決まりました。研究室のほうは、写真をそのまま置いただけで、デザインとして組んでいないと言われました。

> なんでもいいわけではなくデザイン性も考慮して作ってね

> 写真おいただけはNG

### フィジカルAI研究室：主役の違う3案から、お手本のデータの案を選んだ

案は、並べ方の違いではなく、何を主役にするかの違いで3つ出しました。色は3案とも白・黒・赤だけにし、赤は「箱」の意味にそろえています。

| 案 | 主役 | 作り方 |
|---|---|---|
| A 動きの軌跡 | アームがどう動いたか | 実機の動画を白黒で明るく仕上げ、箱が通った道を点線と赤い枠で描く |
| B お手本のデータ | アームが何から動きを覚えたか | 手で教えたお手本（デモ）の関節の動きを線で重ねる。赤い線は箱をつかんでいる間 |
| C 形と面 | ロボットの形 | アームの切り抜きを3段階の色で塗り分け、床を黒い面にしたポスター風 |

【画像】LOG411_02_lab_three_directions.png フィジカルAI研究室のカードの3案を、トップのカードの並びに入れて比べた画像。A は白黒の実機の写真に箱の通り道、B はお手本の関節の動きの線、C はアームをシルク印刷のように塗り分けた絵

B のデータは、KIOXIA に置いてあるお手本のデータ（`datasets/mecharm_redblock_box_teach_v1_trim2`）の19回分です。1回ごとに、7つの値（関節6つとグリッパー）が10分の1秒ごとに記録されています。カードには、いちばん大きく動く2つの関節（J0 は台の回転、J1 は肩）を上下2段に描きました。

B を元に、アームの写真を小さく入れる形も試しました。位置を6通りに変えましたが、最後はアームを入れない形になりました。あわせて、見出しの「×19」も外しています。お手本の回数はこれから増えるので、絵の中に数を焼き込まないためです。

【画像】LOG411_03_lab_arm_positions.png お手本の線の図に、アームの切り抜き写真を置く位置を6通りに変えて並べた画像（右上・右下・左上・2段のあいだ・右に大きく・赤い線の上）

> アームはなし。x１９もなし。これをベースに白黒のツートンにしてみようか

白黒のツートンは、分け方を3通り出しました。左が黒（縦に分ける）、上の段が黒（横に分ける）、右が黒（箱を運ぶあたりから）です。黒い面の上では線を白に、白い面の上では線を黒にし、赤はどちらの面でも同じにしました。

【画像】LOG411_04_lab_twotone.png 白黒ツートンの3案。左が黒、上の段が黒、右が黒

上の段が黒の案を、上下逆（上が白・下が黒）にした形を足して比べ、下が黒の案に決めました。上が白だと、隣のハコブくんの明るい写真と、カードの上の端の明るさがそろいます。

【画像】LOG411_05_lab_twotone_flip.png 上が黒の案（左）と、上下を逆にした下が黒の案（右）を、トップのカードの並びに入れて比べた画像

### 画像認識と物体検出：赤い箱の写真が処理されていく4列にした

画像認識と物体検出のカードも、別の案を頼まれました。このシリーズで実際に撮った写真から、3案を作りました。

| 案 | 元の写真 | 作り方 |
|---|---|---|
| A 処理の段階 | Log03 の赤い箱の写真（4K） | 同じ写真を4列に並べ、画素 → 白黒 → しきい値 → 輪郭 と処理が進む順に見せる |
| B 機械の目 | Log06 の、アームと図形の紙を上から見た写真 | 黒地に輪郭の線だけを残し、見つけた赤い形を点線の枠で囲む |
| C 画素 | Log03 の赤い箱の写真 | 写真を大きなマス目に分け、赤い箱のマスを点線の枠で囲む |

【画像】LOG411_06_vision_three_directions.png 画像認識と物体検出のカードの3案。A は赤い箱が4列で処理されていく図、B は黒地に輪郭の線と赤い形の枠、C は大きなマス目に分けた赤い箱

A に決まりました。ただ、最初の版には、写真の端に写っていた青い箱の角と、紙の継ぎ目が残っていました。

> このへんの消せればA案良さそう

【画像】LOG411_07_vision_blue_corner.png A 案の最初の版の一部。白黒の列の右下に、青い箱の角が灰色の三角として写っている

消し方は、赤い箱とその影だけを元の写真のまま残し、まわりを、写真から作ったなめらかな面に置き換えるやり方です。青い箱の部分を周りの色で埋め（OpenCV の inpaint）、全体を強くぼかして面にしました。そのうえで、赤い箱から少し広げた範囲だけを、境目をぼかして元の写真に戻しています。

- ねらい：シリーズの中身（物を見つける手法を、下の層から1つずつ確かめる）を、カードの絵だけで伝える
- 変わったこと：線画のイラストから、実際に撮った写真が4段階で処理されていく図にした。研究室のカードも、実際のお手本のデータで描いた
- 見込める効果（見込み）：トップからシリーズのページへ進む人が増える。トップの「押したもの」の表で、/series6・/series7 が押された回数を、来週の数字と比べて確かめる

### カードの絵を作るコード

どちらも、元のデータや写真から作り直せるように、サイトの `tools/series_cards/` に置きました。フィジカルAI研究室のカードは、お手本のデータを JSON にまとめたものを読み、SVG に描いてから、Playwright（ブラウザを動かす道具）で画像にします。

#### tools/series_cards/series6.py（フィジカルAI研究室のカード）

```python
"""フィジカルAI研究室（/series6）のトップのカード（3:2・1800×1200）を作る。
お手本19回分の関節の動き（J0・J1）を重ねた線。赤はグリッパーが箱をつかんでいる間。上の段は白、下の段は黒のツートン（2026-10-30）。
元データ: KIOXIA の datasets/mecharm_redblock_box_teach_v1_trim2/raw_episodes/*/episode.npz の state を JSON にまとめたもの（DEMOS に置く）。
出力: tools/series_cards/series6.svg → Playwright で PNG にして public/site/series6-physical-ai-lab-demos.webp
回数は増えるので、絵の中に「×19」のような件数は入れない。
"""
import sys
DEMOS = sys.argv[1] if len(sys.argv) > 1 else 'demos_trim2.json'
import json,numpy as np
d=json.load(open(DEMOS))
W,H=1800,1200
L,R=140,1660
lanes=[(300,620),(760,1080)]
BK='#111111'; WH='#f1f0ec'; RED='#e0262c'
def lines(color,opa):
    out=[]
    for (j,(t,b)) in zip([0,1],lanes):
        allv=np.concatenate([np.array(v)[:,j] for v in d.values()]); lo,hi=allv.min(),allv.max()
        out.append(f'<line x1="{L}" y1="{b+14}" x2="{R}" y2="{b+14}" stroke="{color}" stroke-width="1.5"/>')
        for v in d.values():
            a=np.array(v); n=len(a); g=a[:,6]
            pts=[(L+i/(n-1)*(R-L), b-(a[i,j]-lo)/(hi-lo)*(b-t)) for i in range(n)]
            segs=[];seg=[pts[0]];cur=g[0]
            for p,gg in zip(pts[1:],g[1:]):
                seg.append(p)
                if gg!=cur: segs.append((cur,seg)); seg=[p]; cur=gg
            segs.append((cur,seg))
            for gg,sp in segs:
                path=' '.join(f'{x:.1f},{y:.1f}' for x,y in sp)
                col,op=(RED,'.9') if gg>0.5 else (color,opa)
                out.append(f'<polyline points="{path}" fill="none" stroke="{col}" stroke-opacity="{op}" stroke-width="3.2" stroke-linejoin="round"/>')
    return '\n'.join(out)
def text(color):
    return (f'<text x="{L}" y="150" fill="{color}">DEMONSTRATIONS</text>'
            f'<text x="{R}" y="150" text-anchor="end" fill="{color}">PICK → PLACE</text>')
def make(fn,dark_shape):
    s=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}">',
       '<style>text{font-family:"JetBrains Mono","SFMono-Regular",Menlo,monospace;font-size:40px;letter-spacing:.08em}</style>',
       f'<defs><clipPath id="dk">{dark_shape}</clipPath><mask id="lt"><rect width="{W}" height="{H}" fill="#fff"/><g fill="#000">{dark_shape}</g></mask></defs>',
       f'<rect width="{W}" height="{H}" fill="{WH}"/>',
       f'<g fill="{BK}">{dark_shape}</g>',
       f'<g mask="url(#lt)">{lines(BK,".4")}{text(BK)}</g>',
       f'<g clip-path="url(#dk)">{lines(WH,".5")}{text(WH)}</g>',
       '</svg>']
    open(fn,'w').write('\n'.join(s))
make('tools/series_cards/series6.svg',f'<rect x="0" y="690" width="{W}" height="{H-690}"/>')  # 下の段が黒
```

#### tools/series_cards/series7.py（画像認識と物体検出のカード）

```python
"""画像認識と物体検出（/series7）のトップのカード（3:2・1800×1200）を作る（2026-10-30）。
Log03 で撮った赤い箱の写真を4列に並べ、画素 → 白黒 → しきい値 → 輪郭 と処理が進む順に見せる。
背景の青い箱の角と紙の継ぎ目は、赤い箱と影だけを残して背景をなめらかな面に置き換えて消している。
元の写真: KIOXIA の vision/captured/Log03/IMG_7911.JPG（第1引数で渡す）
出力: 第2引数の PNG → public/site/series7-image-recognition-steps.webp
"""
import sys
import cv2, numpy as np
from PIL import Image, ImageOps, ImageDraw, ImageFont
src=np.array(ImageOps.exif_transpose(Image.open(sys.argv[1])).convert('RGB'))
cx,cy=2195,1019
cw,chh=885,2353
x0=cx-cw//2; y0=max(0,cy-int(chh*0.40))
crop=src[y0:y0+chh,x0:x0+cw]
img=cv2.resize(crop,(450,1200),interpolation=cv2.INTER_AREA)
# 赤い箱（と影）だけ残し、背景はなめらかな面に置き換える（青い箱の角・紙の継ぎ目を消す）
_h=cv2.cvtColor(img,cv2.COLOR_RGB2HSV)
_r=(((_h[...,0]<8)|(_h[...,0]>170))&(_h[...,1]>110)&(_h[...,2]>50)).astype(np.uint8)
_n,_l,_s,_=cv2.connectedComponentsWithStats(_r); _r=(_l==1+np.argmax(_s[1:,4])).astype(np.uint8)
_r=cv2.morphologyEx(_r,cv2.MORPH_CLOSE,np.ones((15,15),np.uint8))
_dark=(cv2.cvtColor(img,cv2.COLOR_RGB2GRAY)<150).astype(np.uint8)
_kill=cv2.dilate(np.maximum(_r,_dark),np.ones((25,25),np.uint8))
_bg=cv2.inpaint(img,_kill,15,cv2.INPAINT_TELEA)
_bg=cv2.GaussianBlur(_bg,(0,0),40)
_keep=cv2.dilate(_r,np.ones((61,61),np.uint8))
_keep[:, :]=_keep*0+_keep  # 箱の近く（影を含む）
_yy,_xx=np.nonzero(_r); _ymax=_yy.max()
_keep[_ymax+40:,:]=0
_a=cv2.GaussianBlur(_keep.astype(np.float32),(0,0),12)[...,None]
img=(img*_a+_bg*(1-_a)).astype(np.uint8)
hsv=cv2.cvtColor(img,cv2.COLOR_RGB2HSV)
red=(((hsv[...,0]<8)|(hsv[...,0]>170))&(hsv[...,1]>110)&(hsv[...,2]>50)).astype(np.uint8)
red=cv2.morphologyEx(red,cv2.MORPH_OPEN,np.ones((5,5),np.uint8)); red=cv2.morphologyEx(red,cv2.MORPH_CLOSE,np.ones((11,11),np.uint8))
n,lab,st,_=cv2.connectedComponentsWithStats(red); k=1+np.argmax(st[1:,4]); red=(lab==k).astype(np.uint8)
gray=cv2.cvtColor(img,cv2.COLOR_RGB2GRAY); g3=np.stack([gray]*3,-1).astype(float)
BK=(17,17,17);WH=(241,240,236);RD=(224,38,44)
t1=g3*(1-red[...,None])+img*red[...,None]
t2=g3
t3=np.where(red[...,None]>0,np.array(WH),np.array(BK)).astype(float)
t4=np.full_like(img,WH)
cnts,_=cv2.findContours(red,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_NONE); c=max(cnts,key=cv2.contourArea)
cv2.drawContours(t4,[c],-1,RD,4,lineType=cv2.LINE_AA)
x,y,w,h=cv2.boundingRect(c)
def dash(im,p0,p1,col,th=2,dl=12):
    p0=np.array(p0,float);p1=np.array(p1,float);L=np.linalg.norm(p1-p0);n=max(2,int(L//dl))
    for i in range(0,n,2):
        a=p0+(p1-p0)*i/n;b=p0+(p1-p0)*min(i+1,n)/n
        cv2.line(im,tuple(a.astype(int)),tuple(b.astype(int)),col,th,cv2.LINE_AA)
p=16
for a,b in [((x-p,y-p),(x+w+p,y-p)),((x+w+p,y-p),(x+w+p,y+h+p)),((x+w+p,y+h+p),(x-p,y+h+p)),((x-p,y+h+p),(x-p,y-p))]: dash(t4,a,b,BK)
M=cv2.moments(c);mx,my=int(M['m10']/M['m00']),int(M['m01']/M['m00'])
cv2.line(t4,(mx-14,my),(mx+14,my),BK,2);cv2.line(t4,(mx,my-14),(mx,my+14),BK,2)
out=np.concatenate([t1,t2,t3,t4.astype(float)],1)
im=Image.fromarray(out.clip(0,255).astype(np.uint8)); d=ImageDraw.Draw(im)
f=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',34)
fs=ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',28)
labs=['PIXELS','GRAY','THRESHOLD','CONTOUR']
for i,l in enumerate(labs):
    col=WH if i==2 else BK
    d.text((i*450+36,1060),f'0{i+1}',font=fs,fill=col)
    d.text((i*450+36,1104),l,font=f,fill=col)
for e in [450,900,1350]: d.line((e,0,e,1200),fill=BK,width=2)
im.save(sys.argv[2])
```

#### src/pages/index.astro の変更（カードの絵の差し替え）

前の絵のファイルは、どれも消さずに残しています。どの絵だったかを、コードのコメントに書きました。iCloud の `site_archive/top_card_thumbnails_20261030/old/` にもコピーを置いています。

```diff
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 1996ff3..0ed2af9 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -18,14 +18,16 @@ import { allManga } from '../lib/records';
 // Studio のトップと同じく、4コマは新しい4話
 const mangaCards = allManga.slice(0, 4);
 
+// 2026-10-30：研究室（series6）はお手本の関節の動きを重ねた白黒ツートンの図（tools/series_cards/series6.py）、導入シミュレーション（series8）は Unity の画面、画像認識（series7）は赤い箱の写真が処理されていく4列の図（tools/series_cards/series7.py）に変えた。
+// 前の絵：研究室 = media の s-1672x941_76620d86-fa7b-491f-b554-99c2f8981d8d.webp、画像認識 = media の s-1800x1200_v-frms_82af05c4-20df-4ea1-b88d-355ed8fabd63.webp、導入シミュレーション = /site/series8-service-robot-simulation.png?v=3（どれも消さずに残してある）
 const series = [
   { href: '/series1', img: 's-1920x1318_v-frms_4cd7792b-68a1-4239-ad08-0785c4e538c5.webp', en: 'Drug Delivery Robot', ja: '薬剤搬送ロボットハコブくん',
     desc: '病院で薬を運ぶロボット「ハコブくん」を題材に、人とロボットの関わり（HRI）を設計する記録。画面の設計、Unity での自律移動、日々の開発の記録を、4コママンガと一緒に残しています。' },
-  { href: '/series6', img: 's-1672x941_76620d86-fa7b-491f-b554-99c2f8981d8d.webp', en: 'Physical AI Lab', ja: 'フィジカルAI研究室',
+  { href: '/series6', img: '/site/series6-physical-ai-lab-demos.webp', en: 'Physical AI Lab', ja: 'フィジカルAI研究室',
     desc: 'ロボットアームが動きを覚える環境を、自宅にひとりで作る記録。お手本の動きを集めてロボット用のAIに学習させ、実機で確かめます。誰でも同じ手順で始められる形で残しています。' },
-  { href: '/series7', img: 's-1800x1200_v-frms_82af05c4-20df-4ea1-b88d-355ed8fabd63.webp', en: 'Image recognition and Object detection', ja: '画像認識と物体検出',
+  { href: '/series7', img: '/site/series7-image-recognition-steps.webp', en: 'Image recognition and Object detection', ja: '画像認識と物体検出',
     desc: 'ロボットのカメラが物を見つける仕組みを、下の層から学び直す記録。画素・色・しきい値・輪郭といった手法を、机の上のアームを撮った写真で1つずつ測って確かめます。' },
-  { href: '/series8', img: '/site/series8-service-robot-simulation.png?v=3', en: 'Service Robot Simulation', ja: 'サービスロボット導入シミュレーション',
+  { href: '/series8', img: '/site/series8-service-robot-unity.webp', en: 'Service Robot Simulation', ja: 'サービスロボット導入シミュレーション',
     desc: '配膳・清掃・搬送などのロボットを店や施設に入れる前に、待ち時間・人数・止まる場所・費用を試算する記録。時間と台数は SimPy、空間と動きは Unity で扱い、前提・結果・コードをすべて公開しています。' },
   { href: '/series5', img: '/site/series5-deep-dive.jpg', // 考察記事のサムネイルと同じ形（3-B）。元の HTML は tools/series5_top_card.html（2026-10-26）
     en: 'Deep Dive', ja: '考察記事',
```


## 作業ログ ― サイトの計測の記録を読んだ

### サイトの計測は3つある

このサイトには、自分で作った計測が3つあります。どれも、見ている人を見分ける情報（IP アドレス・Cookie など）は記録しません。

| 計測 | 何を数えるか | 始めた日 |
|---|---|---|
| サイトの使われ方（/api/ev-stats） | ページを表示した回数、どこまで読んだか、押したもの、どこから来たか。人か機械かの印も付ける | 10/18（人と機械を見分けるのは 10/26 から） |
| クローラーの訪問（/api/cr-stats） | 検索エンジンと AI の巡回プログラムが、どのページを読みに来たか | 10/25 |
| ハコブくんのお知らせ（/api/hk-stats） | 新しい記事のお知らせが出た・押された回数 | 10/17 |

クローラー（crawler）は、検索エンジンや AI の会社が、Web のページを自動で読んで回るプログラムです。名乗り（GPTBot・ClaudeBot など）で、どこの会社の何のためのものかが分かります。ページの中の JavaScript を動かさないものが多いので、サイトの使われ方の計測には出てきません。そのため、ページを返すときに、名乗りを見て別に記録しています。

### 人の閲覧は5日で130回、計測に入った表示の9割は機械だった

| 週（月曜はじまり） | 表示 | 人 | 機械 | 操作なし | 印なし（10/25 より前） |
|---|---|---|---|---|---|
| 10/26〜11/01（10/30 夕方まで） | 1,621 | 130 | 1,434 | 57 | 0 |
| 10/19〜10/25 | 1,292 | 1 | 20 | 1 | 1,270 |
| 10/12〜10/18 | 22 | 0 | 0 | 0 | 22 |

人の閲覧（10/26〜10/30）の中身です。

| 項目 | 値 |
|---|---|
| 表示 | 130回 |
| 表示していた時間の中央値 | 15秒 |
| ページの4分の3まで読んだ割合 | 28% |
| スマホの割合 | 40% |
| 戻るボタンで戻った | 3回 |
| 外のサイトへのリンクを押した | 8回 |
| リンクでない所を押した | 13回 |
| メッセージのページの表示 | 2回（入力を始めた・送信は0） |

来た元です。

| 来たところ | 回数 |
|---|---|
| （直接・不明） | 59 |
| （サイト内） | 27 |
| www.google.com | 26 |
| search.yahoo.co.jp | 8 |
| www.linkedin.com | 4 |
| teams.public.onecdn.static.microsoft | 3 |
| t.co（X） | 3 |
| com.google.android.googlequicksearchbox | 1 |

Microsoft Teams から3回来ています。だれかが社内のチャットにリンクを貼った跡だと思われます。

人がよく見たページです（表示2回以上）。

| ページ | 表示 | 時間の中央値（秒） | 4分の3まで読んだ割合（%） |
|---|---|---|---|
| /gpu-buy-or-rent | 17 | 11 | 12 |
| / | 12 | 22 | 50 |
| /issues | 6 | 12 | 0 |
| /record/service-robot-last-mile | 6 | 18 | 0 |
| /series6 | 4 | 18 | 25 |
| /series4 | 4 | 9 | 50 |
| /record/building-a-physical-ai-lab-at-home | 4 | 14 | 0 |
| /record/beyond-imitation-learning | 4 | 16 | 50 |
| /series8 | 4 | 15 | 0 |
| /record/day145 | 3 | 111 | 33 |
| /record/from-sim-series-to-lp | 3 | 140 | 33 |
| /record/serving-robot-hidden-waits | 3 | 15 | 0 |
| /record/pi05-first-block-in-box | 2 | 11 | 0 |
| /record/rewrite-research-report | 2 | 6 | 0 |
| /record/day17 | 2 | 30 | 100 |
| /message | 2 | 11 | 100 |
| /record/serving-robot-cost-subsidy | 2 | 81 | 0 |
| /record/pi05-mecharm-demo-to-inference | 2 | 17 | 0 |
| /one-year | 2 | 77 | 50 |
| /manga-archive | 2 | 9 | 0 |
| /serving-robot-one-less-staff | 2 | 22 | 50 |
| /record/rewind-and-correct-pi05 | 2 | 106 | 50 |
| /record/road-marking-job-post-and-physical-ai | 2 | 201 | 100 |
| /record/vertical-4koma-for-shorts | 2 | 113 | 50 |
| /record/serving-robot-video-part2 | 2 | 15 | 0 |

1回だけ見られたページのうち、長く読まれたものです。

| ページ | 時間（秒） |
|---|---|
| /record/microfactory-fairino-fr3 | 2,104 |
| /record/day18 | 424 |
| /record/first-search-hit-to-ai-citation | 416 |
| /record/dev-mac-free-space | 351 |
| /record/japan-robot-leader-physical-ai-gap | 206 |
| /record/day12 | 180 |
| /record/ai-readable-openings | 174 |
| /record/youtube-views-to-right-viewers | 159 |

読み取れたことは3つです。

- 来る人がいちばん多いのは導入比較ガイド（GPU を買うか借りるか）で、11秒で離れる人が多い。10/28 に最初の画面へ結論を置いたので、すぐ離れること自体は失敗ではない。次は計算機まで押されたかを見る
- 課題地図（/issues）を開いた6人は、だれもページの4分の1より先にスクロールしていない
- 少数だが、考察寄りの記事（マイクロファクトリー・路面標示の求人とフィジカルAI など）は3〜35分読まれている

### クローラーは今週約7,900回、ChatGPT の利用者の依頼での読み取りは48回

表は、クローラーの記録を始めた 10/25 から 10/30 夕方までの合計です（8,062回。そのうち 10/26 からの週が7,901回）。

| クローラー | だれの・何のため | 読んだ回数 | ページの数 | 記録（LOG）の数 | 見つからない等（400以上） |
|---|---|---|---|---|---|
| Amazonbot | Amazon（Alexa・AI） | 2,035 | 1,311 | 803 | 12 |
| meta-externalagent | Meta（AI） | 1,867 | 982 | 419 | 24 |
| Googlebot | Google（検索） | 1,769 | 594 | 275 | 11 |
| Applebot | Apple（Siri・検索・AI） | 645 | 398 | 64 | 6 |
| ClaudeBot | Anthropic（学習） | 499 | 340 | 64 | 19 |
| GoogleOther | Google（その他） | 337 | 191 | 55 | 0 |
| GPTBot | OpenAI（学習） | 317 | 311 | 48 | 19 |
| bingbot | Bing（検索。ChatGPT の検索・Copilot も使う） | 304 | 141 | 121 | 2 |
| OAI-SearchBot | OpenAI（ChatGPT の検索） | 167 | 71 | 57 | 1 |
| ChatGPT-User | ChatGPT（利用者の依頼で読む） | 48 | 28 | 24 | 0 |
| YandexBot | Yandex（検索） | 24 | 9 | 4 | 0 |
| PerplexityBot | Perplexity（検索） | 21 | 16 | 11 | 0 |
| Bytespider | ByteDance（AI） | 18 | 5 | 2 | 0 |
| Baiduspider | Baidu（検索） | 6 | 3 | 1 | 0 |
| Claude-User | Claude（利用者の依頼で読む） | 2 | 2 | 2 | 0 |
| DuckDuckBot | DuckDuckGo（検索） | 2 | 1 | 0 | 0 |
| Yeti | Naver（検索） | 1 | 1 | 0 | 0 |

この中で、狙いに直結するのは ChatGPT-User です。ChatGPT の利用者が質問したときに、答えを作るためにページを読みに来た記録だからです。Amazon・Meta・Google の大半は、どのサイトにも来る広い巡回で、このサイトが特に評価されているという意味ではありません。クローラーの記録は 10/25 に始めたので、前の週（161回）と比べて増えたとも言えません。

ChatGPT-User と OAI-SearchBot が読んだページです。

| ChatGPT-User が読んだページ | 回数 |
|---|---|
| / | 16 |
| /record/from-sim-series-to-lp | 3 |
| /record/microfactory-fairino-fr3 | 2 |
| /record/serving-robot-cost-subsidy | 2 |
| /serving-robot-one-less-staff | 2 |
| /record/explainer-video-simulation-background | 1 |
| /record/pixelstomotors-com-domain | 1 |
| /record/first-video-end-to-end | 1 |
| /record/lock-down-ai-weekly-writes | 1 |
| /record/youtube-script-curation | 1 |
| /record/dev-mac-free-space | 1 |
| /record/shorts-81-percent-swiped | 1 |

| OAI-SearchBot が読んだページ | 回数 |
|---|---|
| / | 46 |
| /robots.txt | 19 |
| /updates.json | 17 |
| /record/akita-data-center-robotics-perspective | 6 |
| /record/first-video-end-to-end | 5 |
| /record/pi05-libero-benchmark-first-run | 5 |
| /record/issues-map-weekly-update | 4 |
| /record/day24 | 2 |
| /record/video-progress-board | 2 |
| /record/lock-down-ai-weekly-writes | 1 |
| /record/from-sim-series-to-lp | 1 |
| /record/dev-mac-free-space | 1 |

【図解】LOG411_crawler_readers.html

### なぜ人より先に AI に読まれているのか

> 「人に見られるサイト」より「AI に読まれるサイト」として先に育って
> これは何が要因だと思われますか？こちらの意図と合っていますか？

と聞かれ、考えられる要因を3つ挙げました。

1. AI に読ませる準備を先にしてきた。検索に出さない設定の解除とサイトマップ（10/17）、AI 向けの案内 /llms.txt、公開をすぐ検索エンジンに知らせる IndexNow（10/28）、記事を AI が読む前提で書く（長いコードを別ファイルに分ける・最初の1文を問いへの答えにする）
2. 毎日記事を出している。更新の多いサイトには、クローラーが何度も戻ってくる。トップ・robots.txt・updates.json・サイトマップへの訪問が上位に来ている
3. 人の入口がまだ細い。新しいドメインに移って2週間ほどで、検索の評価がまだ付いていない。SNS から来たのは LinkedIn 4回・X 3回

意図とは合っています。記事は人より AI が読む前提で書き、AI 経由で読まれる数を、主指標の「再現可能な環境数」と同じくらい大事にしているからです。**ただ、AI が読んだあとに人が来ているかは見えていません。** ChatGPT の画面のリンクから来た人は、来た元に chatgpt.com が出るはずですが、今週は0回でした。


## 作業ログ ― AI から来た人を数えるようにした

### utm_source は、リンクに付く「来た元の印」

utm_source は、リンクの住所の後ろに付ける `?utm_source=名前` の印です。もともとは広告やメールのリンクがどこから押されたかを数えるために使われてきたもので、ChatGPT は回答の中のリンクに `?utm_source=chatgpt.com` を付けます（[OpenAI の説明](https://help.openai.com/en/articles/12627856-publishers-and-developers-faq)）。

| 言葉 | 意味 |
|---|---|
| referrer（リファラー） | ブラウザが「どのページから来たか」をサイトに伝える情報。アプリから開いたときなどは送られない |
| utm_source | リンクの住所に付ける来た元の印。リンクを作った側が付けるので、referrer が送られなくても残る |

これまでのサイトの計測は、referrer だけで来た元を決めていました。スマホの ChatGPT のアプリからリンクを開くと、referrer が送られないことが多く、そのまま「直接・不明」（今週59回で最多）に入ります。

### 印の値だけを記録し、「どこから来たか」の表で分けて出す

外から入った最初の表示のときだけ、utm_source の値を記録するようにしました。値は英小文字・数字・`.`・`_`・`-` の40字までで、それ以外は捨てます。「どこから来たか」の表では、referrer がなく印があるときに「chatgpt.com（リンクの印）」として数え、印だけをまとめた表も足しました。記録する項目が増えたので、プライバシーポリシーにも書き足しました。

確かめは2段でしました。受ける側は、Node で worker を読み込み、R2 をまねた入れ物に記録を入れて、表の数え方を見ました。送る側は、Playwright でページを開き、送られる中身を取り出しました。

```text
（受ける側：chatgpt.com の印・Google から・おかしな印・何もなし の4件を送った）
204
204
204
204
[{"from":"（直接・不明）","count":2},{"from":"chatgpt.com（リンクの印）","count":1},{"from":"www.google.com","count":1}] [{"source":"chatgpt.com","count":1}]

（送る側：ページを開いて、送られた中身の p と us を出した）
/series6.html?utm_source=chatgpt.com → p= /series6.html us= "chatgpt.com" r= ""
/series7.html → p= /series7.html us= "" r= ""
/?utm_source=%3Cx%3E → p= / us= "" r= ""
```

`<x>` のようなおかしな印は記録されず、「直接・不明」に入りました。

- ねらい：AI が読んだあと、人がサイトに来ているかを見えるようにする
- 変わったこと：「直接・不明」に混ざっていた、AI の回答のリンクから来た人が「chatgpt.com（リンクの印）」として分かれて出る
- 見込める効果（見込み）：来週の「どこから来たか」の表で、印の付いた来訪が出るかを見る。出なければ、AI は読んでいるがリンクを押されていない、と分かる

#### worker/index.js・SiteMeasure.astro・privacypolicy.astro の変更（utm_source とクローラーの表）

```diff
commit 34f2ba054d5bdd06d5d61891318a3a197c68f738
Author: Claude <noreply@anthropic.com>
Date:   Fri Oct 30 17:22:18 2026 +0900

    計測：リンクの来た元の印（utm_source）を記録、クローラーの表に返せなかったページを追加
    
    AI の回答のリンク（?utm_source=chatgpt.com）から来た人が「直接・不明」に混ざらないようにする。
    プライバシーポリシーに記録する項目を追記。
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXSDAgg3MAhAQWAaaXy

diff --git a/src/components/SiteMeasure.astro b/src/components/SiteMeasure.astro
index 8f07cef..1bea227 100644
--- a/src/components/SiteMeasure.astro
+++ b/src/components/SiteMeasure.astro
@@ -61,10 +61,18 @@
         return { f: '', r: u.hostname.slice(0, 80) };
       } catch { return { f: '', r: '' }; }
     };
+    // リンクに付いた来た元の印（utm_source。ChatGPT などの AI は回答のリンクに ?utm_source=chatgpt.com を付ける）。
+    // アプリから開くと referrer が送られず「直接・不明」に混ざるため、印の値だけを送る（2026-10-30）
+    const utm = () => {
+      try {
+        const v = (new URLSearchParams(location.search).get('utm_source') || '').toLowerCase();
+        return /^[a-z0-9._-]{1,40}$/.test(v) ? v : '';
+      } catch { return ''; }
+    };
     const fresh = (from?: string) => {
       const o = origin();
       return {
-        v: newId(), n: 0, p: location.pathname.slice(0, 160), f: from ?? o.f, r: from ? '' : o.r,
+        v: newId(), n: 0, p: location.pathname.slice(0, 160), f: from ?? o.f, r: from ? '' : o.r, us: from ? '' : utm(),
         w: window.innerWidth < 600 ? 'sp' : 'pc', s: 0, t: 0, c: [] as Click[],
         d: 0, g: 0, cp: 0, vp: 0, fs: 0, fx: 0, fv: 0,
         // 人と機械を分ける印（2026-10-25）。u＝人の操作があった、wd・hl＝自動で動くブラウザの印（名乗りそのものは送らない）
diff --git a/src/pages/privacypolicy.astro b/src/pages/privacypolicy.astro
index 5fd396a..2644106 100644
--- a/src/pages/privacypolicy.astro
+++ b/src/pages/privacypolicy.astro
@@ -7,7 +7,7 @@ const sections = [
   ['メッセージフォームでお預かりする情報', 'メッセージフォームから送信いただいた内容（お名前・メールアドレス・ご相談内容）と、送信日時・ご利用のブラウザの種類（ユーザーエージェント）を保存します。これらはお問い合わせへのご返信とご相談への対応のためにのみ使用します。\n保存には Cloudflare, Inc. のサービス（Cloudflare R2）を、当方への通知メールの送信には Cloudflare Email Routing を利用しています。'],
   ['Cookie とアクセス解析について', '当サイトは、サイトの改善のために Cloudflare, Inc. のアクセス解析（Cloudflare Web Analytics）を利用しています。このアクセス解析は Cookie を使わず、ページの閲覧数・表示速度・参照元・おおよその国・ブラウザやOSの種類などを、個人を識別しない形で集計します。<br>当サイトは広告配信サービスを利用しておらず、当方がお客さまを識別するための Cookie を設定することはありません。'],
   ['更新のお知らせの記録について', '当サイトでは、新しい記事などが公開されたときにキャラクター（ハコブくん）が画面に出てお知らせします。このお知らせが役に立っているかを確かめるため、「お知らせが出た」「お知らせを押した」「お知らせを押さずに7日以内にその記事を開いた」という出来事を、どの記事のお知らせか・どのページで出たか・画面の幅（広い／狭い）・日付とあわせて記録します。\nIP アドレス・ブラウザの種類・Cookie などは記録せず、閲覧者を見分ける番号も作りません。同じお知らせを繰り返し出さないため、お知らせ済みの記事をお客さまのブラウザ（ローカルストレージ）に覚えておきますが、この情報は当方には送られません。'],
-  ['サイトの使われ方の記録について', '当サイトでは、読みやすさや使いやすさを改善するため、ページを1回表示するごとに、次の内容を記録します。\n・表示したページと、サイト内のどのページから来たか（戻るボタンで戻ったかどうか）。外のサイトから来た場合は、そのサイトの名前（ドメイン）だけ\n・画面の幅（広い／狭い）、ページをどこまで読み進めたか、ページを表示していた時間\n・押したリンクやボタン（行き先と、ページのどの部分にあったか）、リンクでない所を押した回数、短い間に同じ所を何度も押した回数\n・文章をコピーした回数、動画を再生した回数、メッセージフォームに入力を始めたか・送信ボタンを押した回数\n・人による閲覧か、検索エンジンのクローラーなど自動で巡回するプログラムによる表示か（マウス・指・キーの操作があったかどうかと、ブラウザが自動で動いているかの判定結果だけ）\n・日付\nフォームに入力された内容やコピーした文章そのものは記録しません。IP アドレス・ブラウザの種類・Cookie も記録しません（自動のプログラムかどうかの判定にブラウザの名乗りを使いますが、名乗りそのものは保存しません）。記録をまとめるために1回の表示ごとに一時的な番号を作りますが、この番号はブラウザに保存されず、別のページや次回の来訪とは結び付きません。記録の保存には Cloudflare, Inc. のサービス（Cloudflare R2）を利用しています。\nブラウザで「トラッキングしない」（Do Not Track）または Global Privacy Control を設定している場合は、この記録を行いません。'],
+  ['サイトの使われ方の記録について', '当サイトでは、読みやすさや使いやすさを改善するため、ページを1回表示するごとに、次の内容を記録します。\n・表示したページと、サイト内のどのページから来たか（戻るボタンで戻ったかどうか）。外のサイトから来た場合は、そのサイトの名前（ドメイン）だけ。リンクに来た元の印（utm_source。例：chatgpt.com）が付いていた場合は、その印の値\n・画面の幅（広い／狭い）、ページをどこまで読み進めたか、ページを表示していた時間\n・押したリンクやボタン（行き先と、ページのどの部分にあったか）、リンクでない所を押した回数、短い間に同じ所を何度も押した回数\n・文章をコピーした回数、動画を再生した回数、メッセージフォームに入力を始めたか・送信ボタンを押した回数\n・人による閲覧か、検索エンジンのクローラーなど自動で巡回するプログラムによる表示か（マウス・指・キーの操作があったかどうかと、ブラウザが自動で動いているかの判定結果だけ）\n・日付\nフォームに入力された内容やコピーした文章そのものは記録しません。IP アドレス・ブラウザの種類・Cookie も記録しません（自動のプログラムかどうかの判定にブラウザの名乗りを使いますが、名乗りそのものは保存しません）。記録をまとめるために1回の表示ごとに一時的な番号を作りますが、この番号はブラウザに保存されず、別のページや次回の来訪とは結び付きません。記録の保存には Cloudflare, Inc. のサービス（Cloudflare R2）を利用しています。\nブラウザで「トラッキングしない」（Do Not Track）または Global Privacy Control を設定している場合は、この記録を行いません。'],
   ['外部サービスの利用について', '当サイトでは、表示や機能のために次の外部サービスを利用しています。これらのサービスの提供元は、お客さまのブラウザから送られる情報（IP アドレス、ブラウザの種類など）を、それぞれのプライバシーポリシーに基づいて取得することがあります。\n・Cloudflare（サイトの配信・画像と動画の配信・メッセージの保存・アクセス解析・サイトの使われ方の記録）：<a href="https://www.cloudflare.com/ja-jp/privacypolicy/" target="_blank" rel="noopener">Cloudflare のプライバシーポリシー</a>\n・Google Fonts（文字の表示）／YouTube（動画の埋め込み）：<a href="https://policies.google.com/privacy?hl=ja" target="_blank" rel="noopener">Google プライバシーポリシー</a>\nYouTube の動画を再生した場合、YouTube が Cookie を設定することがあります。'],
   ['個人情報の安全対策', '当方は、個人情報の正確性及び安全性確保のために、セキュリティに万全の対策を講じています。\n不正な大量送信を防ぐため、メッセージフォームなどへの送信の回数を、送信元の IP アドレスごとに短い時間だけ数えています（Cloudflare の流量制限）。IP アドレスはこの数えのためだけに使い、記録はしません。'],
   ['ご本人の照会', 'お客さまがご本人の個人情報の照会・修正・削除などをご希望される場合には、<a href="/message">メッセージフォーム</a>からご連絡ください。ご本人であることを確認の上、対応させていただきます。'],
diff --git a/worker/index.js b/worker/index.js
index 05e7f90..2c44687 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -290,6 +290,7 @@ h1{font-size:20px}h2{font-size:16px;margin-top:28px}table{border-collapse:collap
 <h2>日ごと</h2>${table('日付', data.byDay, 'day')}
 <h2>更新ごと</h2>${table('更新', data.byItem, 'id')}
 <h2>画面の幅（pc＝広い／sp＝スマホ）</h2>${table('幅', data.byWidth, 'w')}
+<h2>返せなかったページ（400以上）</h2>${table([['path', 'ページ'], ['status', '返した結果'], ['visits', '回数'], ['bots', 'クローラー'], ['last', '最後の日']], data.errors)}
 <p>記録の件数：${data.total}</p></body></html>`;
   return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8', 'x-robots-tag': 'noindex', 'cache-control': 'no-store' } });
 }
@@ -332,6 +333,7 @@ async function evRecord(request, env, url) {
     v, n: String(evInt(d.n, 50)), p,
     f: f === '(戻る)' || (f.startsWith('/') && f.length <= 160) ? f : '',
     r: /^[a-z0-9.-]{1,80}$/i.test(String(d.r || '')) ? String(d.r).toLowerCase() : '',
+    us: /^[a-z0-9._-]{1,40}$/.test(String(d.us || '')) ? String(d.us) : '',
     w: d.w === 'sp' ? 'sp' : 'pc',
     s: String(EV_DEPTHS.has(s) ? s : 0),
     t: String(evInt(d.t, 7200)),
@@ -425,7 +427,7 @@ async function evStats(request, env, url) {
   // ページごと・移り方・押したもの（week= を付けるとその週だけ。付けなければ全期間）
   const scope = week ? counted.filter((r) => r.week === week) : counted;
   const count = (map, k, add = 1) => map.set(k, (map.get(k) || 0) + add);
-  const pagesMap = new Map(), moves = new Map(), refs = new Map(), targets = new Map(), areas = new Map(), dead = new Map();
+  const pagesMap = new Map(), moves = new Map(), refs = new Map(), utms = new Map(), targets = new Map(), areas = new Map(), dead = new Map();
   for (const r of scope) {
     const pg = pagesMap.get(r.p) || { path: r.p, views: 0, times: [], s25: 0, s50: 0, s75: 0, s100: 0, dead: 0, rage: 0, copies: 0, plays: 0, out: 0 };
     pg.views++; pg.times.push(r.t);
@@ -433,7 +435,9 @@ async function evStats(request, env, url) {
     pg.dead += r.d; pg.rage += r.g; pg.copies += r.cp; pg.plays += r.vp; pg.out += r.clicks.filter(outside).length;
     pagesMap.set(r.p, pg);
     if (r.f) count(moves, `${r.f} → ${r.p}`);
-    count(refs, r.r || (r.f ? '（サイト内）' : '（直接・不明）'));
+    // referrer がなくてもリンクの印（utm_source）があれば、その名前で数える（2026-10-30）
+    count(refs, r.r || (r.us ? `${r.us}（リンクの印）` : (r.f ? '（サイト内）' : '（直接・不明）')));
+    if (r.us) count(utms, r.us);
     for (const c of r.clicks) {
       count(areas, c.area);
       if (c.target.startsWith('(リンクなし)')) count(dead, `${r.p} ${c.target}`);
@@ -451,6 +455,7 @@ async function evStats(request, env, url) {
     })),
     moves: top(moves, 50, 'move'),
     referrers: top(refs, 30, 'from'),
+    utmSources: top(utms, 30, 'source'),
     clicks: top(targets, 60, 'target'),
     areas: [...areas].sort((a, b) => b[1] - a[1]).map(([a, n]) => ({ area: EV_AREA_JA[a] || a, count: n })),
     deadClicks: top(dead, 30, 'where'),
@@ -471,6 +476,7 @@ h1{font-size:20px}h2{font-size:16px;margin-top:28px}.wrap{overflow-x:auto}table{
 <h2>ページごと</h2>${table([['path', 'ページ'], ['views', '表示'], ['medianSeconds', '表示時間（中央値・秒）'], ['read25Pct', '25％'], ['read50Pct', '50％'], ['read75Pct', '75％'], ['read100Pct', '最後まで'], ['deadClicks', 'リンクなしを押した'], ['rageClicks', '連打'], ['copies', 'コピー'], ['videoPlays', '動画の再生'], ['outClicks', '外へのクリック']], data.byPage)}
 <h2>ページからページへの移り方</h2>${table([['move', '移り方'], ['count', '回数']], data.moves)}
 <h2>どこから来たか</h2>${table([['from', '来たところ'], ['count', '回数']], data.referrers)}
+<h2>リンクに付いた来た元の印（utm_source。2026-10-30 から）</h2><p>ChatGPT などの AI は、回答の中のリンクに ?utm_source=chatgpt.com のような印を付ける。アプリから開くと「どこから来たか」が送られないので、この印で AI から来た人を数える。</p>${table([['source', '印'], ['count', '回数']], data.utmSources)}
 <h2>押したリンク・ボタン（行き先｜ページのどの部分か）</h2>${table([['target', '押したもの'], ['count', '回数']], data.clicks)}
 <h2>押した場所（ページのどの部分か）</h2>${table([['area', '部分'], ['count', '回数']], data.areas)}
 <h2>リンクでない所を押した（ページと、押したものの種類）</h2>${table([['where', '場所'], ['count', '回数']], data.deadClicks)}
@@ -548,6 +554,14 @@ async function crStats(request, env, url) {
   const weeks = new Map();
   for (const r of rows) { const w = weeks.get(r.week) || new Map(); count(w, r.b); weeks.set(r.week, w); }
   // ページごと（?bot= でそのクローラーだけ）
+  // 返せなかった訪問（400以上）をページ・結果ごとに（2026-10-30）
+  const errMap = new Map();
+  for (const r of rows) {
+    if (Number(r.s) < 400) continue;
+    const k = `${r.p}\t${r.s}`;
+    const e = errMap.get(k) || { path: r.p, status: r.s, visits: 0, bots: new Set(), last: '' };
+    e.visits++; e.bots.add(r.b); if (r.day > e.last) e.last = r.day; errMap.set(k, e);
+  }
   const scope = only ? rows.filter((r) => r.b === only) : rows;
   const paths = new Map(), pathBots = new Map();
   for (const r of scope) { count(paths, r.p); const s = pathBots.get(r.p) || new Set(); s.add(r.b); pathBots.set(r.p, s); }
@@ -558,6 +572,7 @@ async function crStats(request, env, url) {
     })),
     byWeek: [...weeks].sort((a, b) => b[0].localeCompare(a[0])).map(([week, m]) => ({ week, total: [...m.values()].reduce((x, y) => x + y, 0), bots: [...m].sort((a, b) => b[1] - a[1]).map(([b, n]) => `${b} ${n}`).join('、') })),
     byPage: [...paths].sort((a, b) => b[1] - a[1]).slice(0, 100).map(([path, n]) => ({ path, visits: n, bots: [...(pathBots.get(path) || [])].join('、') })),
+    errors: [...errMap.values()].sort((a, b) => b.visits - a.visits).slice(0, 100).map((e) => ({ path: e.path, status: e.status, visits: e.visits, bots: [...e.bots].join('、'), last: e.last })),
   };
   if (url.searchParams.get('format') === 'json') {
     return new Response(JSON.stringify(data), { headers: { 'content-type': 'application/json; charset=utf-8', 'x-robots-tag': 'noindex', 'cache-control': 'no-store' } });
@@ -573,6 +588,7 @@ h1{font-size:20px}h2{font-size:16px;margin-top:28px}.wrap{overflow-x:auto}table{
 <h2>クローラーごと</h2>${table([['bot', '名前'], ['who', 'だれの・何のため'], ['visits', '読んだ回数'], ['pages', 'ページの数'], ['records', '記録（LOG）の数'], ['verified', 'Cloudflare が確かめた'], ['unverified', '確かめられなかった'], ['errors', '見つからない等（400以上）'], ['last', '最後に来た日']], data.byBot)}
 <h2>週ごと</h2>${table([['week', '週（月曜）'], ['total', '合計'], ['bots', '内訳']], data.byWeek)}
 <h2>よく読まれたページ${only ? `（${esc(only)} だけ）` : ''}</h2>${table([['path', 'ページ'], ['visits', '回数'], ['bots', '読みに来たクローラー']], data.byPage)}
+<h2>返せなかったページ（400以上）</h2>${table([['path', 'ページ'], ['status', '返した結果'], ['visits', '回数'], ['bots', 'クローラー'], ['last', '最後の日']], data.errors)}
 <p>記録の件数：${data.total}</p></body></html>`;
   return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8', 'x-robots-tag': 'noindex', 'cache-control': 'no-store' } });
 }
```


## 作業ログ ― クローラーが開けなかったページを特定して直した

### 返せなかったページを、クローラーの表に足した

クローラーの表には、返せなかった訪問の「回数」しか出ていませんでした。上の変更で、ページと結果（404 など）ごとの一覧も出るようにしました。公開後に開いた一覧です。

| 回数 | 結果 | ページ | クローラー |
|---|---|---|---|
| 6 | 404 | /manga | ClaudeBot、GPTBot、Googlebot、OAI-SearchBot、Amazonbot |
| 5 | 404 | /ai-agent | meta-externalagent、Googlebot |
| 3 | 404 | /record/%7BR_%5Bn%5D%7D | ClaudeBot、GPTBot、Amazonbot |
| 3 | 404 | /record/%7BR_%5Blg%5D%7D | ClaudeBot、GPTBot、Amazonbot |
| 3 | 404 | /record/precision-is-not-learning | meta-externalagent |
| 3 | 404 | /english-4-koma/ep9 | meta-externalagent |
| 3 | 404 | /prototype6 | meta-externalagent、Googlebot |
| 3 | 404 | /files/LOG388/%7Bs.img.startsWith('/') | GPTBot、ClaudeBot、Amazonbot |
| 3 | 404 | /record/side-camera-pipeline-split | meta-externalagent |
| 3 | 404 | /prototype3 | meta-externalagent、Googlebot |
| 3 | 404 | /files/LOG361/18_AI_%E2%80%94state-from-target.txt | Googlebot、Applebot |
| 3 | 404 | /files/LOG361/01_camera_align.py_%E2%80%94episode.txt | Applebot、Googlebot |
| 3 | 404 | /files/LOG395/%7Bm%5Bl%5D%7D | ClaudeBot、GPTBot、Amazonbot |
| 3 | 404 | /files/LOG395/%7Bc%7D | ClaudeBot、GPTBot、Amazonbot |
| 3 | 404 | /files/LOG395/%7Bo%7D | ClaudeBot、GPTBot、Amazonbot |
| 2 | 404 | /files/LOG378/'%20%2B%20LOGO%20%2B%20' | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG382/'%20%2B%20LOGO%20%2B%20' | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG385/%7B%60/%24%7BseriesPage%5B0%5D%7D%60%7D | ClaudeBot、GPTBot |
| 2 | 404 | /prototype5 | meta-externalagent |
| 2 | 404 | /files/LOG388/%7B%60/%24%7BseriesPage%5B0%5D%7D%60%7D | GPTBot、ClaudeBot |
| 2 | 404 | /prototype1 | meta-externalagent |
| 2 | 404 | /prototype2 | meta-externalagent |
| 2 | 404 | /manga/ep9 | meta-externalagent |
| 2 | 404 | /files/LOG361/12_1_%E2%80%94via-stride.txt | Googlebot、Applebot |
| 2 | 404 | /files/LOG361/13_1_%E2%80%94via-stride.txt | Applebot、Googlebot |
| 2 | 404 | /files/LOG373/%7Bs.img.startsWith('/') | Amazonbot |
| 2 | 404 | /files/LOG395/%7BEI%5Bp%5D%5B0%5D%7D | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG395/'%20%2B%20LOGO%20%2B%20' | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG395/%7Be%5B0%5D%7D | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG395/%7Be%5B1%5D%7D | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG395/%7B_c%7D | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG395/%7B_o%7D | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG399/%7BCTA%7D | ClaudeBot、GPTBot |
| 2 | 404 | /files/LOG399/%7B%60/%24%7BseriesPage%5B0%5D%7D%60%7D | ClaudeBot、GPTBot |
| 1 | 404 | /BingSiteAuth.xml | bingbot |
| 1 | 404 | /prototype4 | meta-externalagent |
| 1 | 404 | /files/LOG361/11_1_%E2%80%94via-stride.txt | Googlebot |
| 1 | 404 | /files/LOG371/%7B%60 | Amazonbot |
| 1 | 404 | /files/LOG373/%7Bhref(l)%7D | Amazonbot |
| 1 | 404 | /record/ | bingbot |

3種類に分かれました。

| 種類 | 例 | 扱い |
|---|---|---|
| サイトの不具合 | /files/LOG361/11_1_—via-stride.txt | 直した（下の節） |
| 古い住所 | /manga・/prototype1〜6・/english-4-koma/ep9 | 行き先がはっきりしたものだけ転送した（次の作業ログ） |
| コードの文字列を住所と勘違いしたもの | /files/LOG395/{c}・/record/{R_[n]} | 存在しない住所なので、404 を返すのが正しい。直さない |

### 記事のファイルの枠で、「--」が「—」に置き換わっていた

LOG361 の記事には、`11_1_--via-stride.txt` のように、名前に `--` を含むファイルが5本あります。記事の Markdown を HTML に変える途中で、`--` が全角のダッシュ「—」に置き換わっていました。読みやすい記号に直す仕組み（smartypants）が、ファイル名にまで効いていたためです。そのため、ファイルの枠のリンクは存在しない住所を指し、押しても開けず、枠の中の先頭の数行も空でした。クローラーだけでなく、人にとっても壊れていたリンクです。

直し方は、枠を作るときに、ファイルが見つからなければ「—」を「--」に、「–」を「-」に戻して探し直す形にしました。

#### src/lib/remark-markers.mjs の変更

```diff
commit 4fa96239b1ce36f67e749394b6159c29f637a1f9
Author: Claude <noreply@anthropic.com>
Date:   Fri Oct 30 17:29:42 2026 +0900

    記事のファイルの枠：ファイル名の「--」が「—」に置き換わってリンクが開けなかったのを直す（LOG361 の5本）
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXSDAgg3MAhAQWAaaXy

diff --git a/src/lib/remark-markers.mjs b/src/lib/remark-markers.mjs
index 56978c8..ce69d3b 100644
--- a/src/lib/remark-markers.mjs
+++ b/src/lib/remark-markers.mjs
@@ -82,6 +82,12 @@ export default function remarkMarkers() {
       }
       m = text.match(/^【ファイル】\s*(LOG\d+\/\S+)\s+(\S+)\s+(\S+)$/);
       if (m) {
+        // 読み込みの途中で「--」が「—」（「-」が「–」）に置き換わることがある。そのままでは
+        // 「11_1_--via-stride.txt」のようなファイル名が変わってリンクが開けないので、元のファイル名に戻す（2026-10-30）
+        if (!existsSync(`public/files/${m[1]}`)) {
+          const back = m[1].replace(/—/g, '--').replace(/–/g, '-');
+          if (existsSync(`public/files/${back}`)) m[1] = back;
+        }
         const href = `/files/${m[1]}`;
         const name = m[1].split('/').pop();
         const disk = `public${href}`;
```

### 直しが本番に出なかった：Astro の記事の作り置き

直してビルドしても、手元のページは変わりませんでした。Astro（このサイトを作っている仕組み）は、記事の中身が変わらないと、前に変換した結果（`node_modules/.astro/data-store.json`）を使い回すためです。変換の仕組みのほうを直しても、記事の中身は同じなので、作り置きがそのまま使われていました。

手元では作り置きを消して直りましたが、本番に出しても直りませんでした。Cloudflare のビルドも、この作り置きを次のビルドへ持ち越していたためです。

そこで、ビルドの前に動く小さなスクリプトを足しました。`src/lib` の remark・rehype のプラグインの中身からハッシュ（中身の指紋）を作り、前のビルドのときと違えば作り置きを捨てます。同じなら何もしないので、ふだんのビルドは速いままです。

#### tools/clear_content_cache.mjs

```javascript
// 記事の変換の仕組み（src/lib の remark・rehype のプラグイン）を変えたときだけ、Astro の記事の作り置き（data-store.json）を捨てる（2026-10-30）。
// Astro は記事の中身が変わらないと前の変換結果を使い回すため、プラグインを直しても公開中のページに反映されなかった（Cloudflare のビルドも作り置きを持ち越す）。
// package.json の build で astro build の前に動く。プラグインが変わっていなければ何もしない。
import { createHash } from 'node:crypto';
import { readFileSync, readdirSync, existsSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
const files = readdirSync('src/lib').filter((f) => /^(remark|rehype)-.*\.m?js$/.test(f)).sort();
const h = createHash('sha256');
for (const f of files) h.update(f).update(readFileSync(`src/lib/${f}`));
const now = h.digest('hex');
const mark = 'node_modules/.astro/markdown-plugins.sha256';
const before = existsSync(mark) ? readFileSync(mark, 'utf-8').trim() : '';
if (before !== now) {
  for (const p of ['node_modules/.astro/data-store.json', '.astro/data-store.json']) if (existsSync(p)) rmSync(p);
  mkdirSync('node_modules/.astro', { recursive: true });
  writeFileSync(mark, now);
  console.log(`記事の変換の仕組みが変わったので、記事の作り置きを捨てた（${files.join('、')}）`);
}
```

#### package.json の変更

```diff
commit 27b263670cb261ba19210eff1faa3d29d2919427
Author: Claude <noreply@anthropic.com>
Date:   Fri Oct 30 17:33:41 2026 +0900

    ビルド：記事の変換の仕組みを変えたときだけ Astro の記事の作り置きを捨てる（直しが本番に反映されなかったため）
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01LevEXSDAgg3MAhAQWAaaXy

diff --git a/package.json b/package.json
index cc6f497..c695f91 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "type": "module",
   "private": true,
   "scripts": {
-    "build": "astro build",
+    "build": "node tools/clear_content_cache.mjs && astro build",
     "dev": "astro dev",
     "preview": "astro preview"
   },
```

公開後に本番のページを開いて、5本とも正しい住所になり、ファイルが開けることを確かめました。

```text
data-src="/files/LOG361/01_camera_align.py_--episode.txt"
data-src="/files/LOG361/11_1_--via-stride.txt"
data-src="/files/LOG361/12_1_--via-stride.txt"
data-src="/files/LOG361/13_1_--via-stride.txt"
data-src="/files/LOG361/18_AI_--state-from-target.txt"
200
```


## 作業ログ ― 古い住所を今のページへ転送した

### 301 転送は「引っ越し先を知らせる」返事

301 転送は、ページに来た人やクローラーに「このページは引っ越した。新しい住所はこちら」と伝える返事です。ブラウザは自動で新しい住所を開き、検索エンジンは古い住所の評価を新しい住所へ移します（[Google の説明](https://developers.google.com/search/docs/crawling-indexing/301-redirects)）。一度出すと、ブラウザや検索エンジンが覚えるので、あとで戻しにくい変更です。

### 行き先がはっきりしたものだけを転送した

| 古い住所 | 転送先 | 理由 |
|---|---|---|
| /manga・/manga/ | /manga-archive | 4コマの一覧は /manga-archive。/manga という一覧ページはない |
| /manga/ep9 のような1桁の話数 | /manga/ep09 | 話数の住所は2桁。ページがある話だけ転送する |
| /english-4-koma/ep9 のような1桁 | /english-4-koma/ep09 | 同上（英語版） |
| /record/precision-is-not-learning（末尾の / もあり） | /record/precision-is-not-learning-so101 | 記事の住所の末尾が変わっていた |
| /prototype1〜6（末尾の / もあり） | /series1〜6 | 前はシリーズのページの住所が /prototype だった |

最初の案では、/ai-agent と /prototype1〜6 は、出どころが分からないので 404 のままにしていました。確かめた範囲で手がかりがなかったためです（サイトのアーカイブ、iCloud の作業ファイル、Web 検索）。

> /ai-agentは今は使っていません。Difyを使ってAIエージェントを動かしてました。
> /prototype1〜6は前は違ったslugだった可能性があります

> 前はシリーズページがprototypeでした

/ai-agent は使っていないページなので、404 のままにしました。/prototype1〜6 は、同じ番号の /series1〜6 へ転送することにしました。

### 住所を変える変更なので、codex のレビューを通した

このプロジェクトでは、公開したあとに戻しにくい変更（住所・転送・フォーム・データの消去）は、公開の前に外部の AI のレビューを通す決まりです。同じ書き手で書いて確かめると、同じ見落としをするためです。

[codex](https://github.com/openai/codex)（OpenAI のコードを書く AI を、Mac のターミナルで動かす道具）に、読み取りだけの設定でレビューしてもらいました。渡し方は、Mac の「依頼箱」（Claude が置いた依頼を Claude Code に自動で渡す仕組み）を使い、Claude Code が codex に依頼文を渡して、答えをそのまま返す形です。

レビューを待つ間は、転送の変更を main とは別のブランチ（redirect-old-paths）に置き、公開していません。

#### codex のレビュー依頼

````text
# コードレビュー依頼：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（該当行と修正案）で答え、最後に「公開してよいか」「公開前に直すべき項目」をまとめてください。
````

#### codex のレビュー結果（Claude Code の報告から、手を加えずに）

````text
**公開前に、存在しない話への 301 だけ絞り込むことを勧めます。** 条件の範囲と処理順は概ね適切で、提示された差分自体に転送ループはありません。

以下は提示された差分・設定・検証結果に基づくレビューです。実際のビルド成果物や既存の転送設定までは確認していません。

### A1. 転送条件：✅ OK

完全一致と正規表現で、対象を十分限定できています。

| リクエスト | この追加処理での結果 |
|---|---|
| `/manga/ep9` | `/manga/ep09` へ301 |
| `/manga/ep90` | 対象外 |
| `/manga/ep09` | 対象外 |
| `/manga-archive` | 対象外 |
| `/manga/ep9/` | 対象外 |
| `/prototype6/` | `/series6` へ301 |
| `/prototype7`・`/prototype10` | 対象外 |
| `/prototype1/foo` | 対象外 |

`url.pathname` で判定するためクエリは条件に影響せず、`to.search = url.search` で保持されます。転送先は固定の同一オリジン内パスなので、クエリで外部サイトへ転送先を差し替える問題もありません。

`/manga/ep9/` を対象外にするのは、今回の表どおりです。末尾スラッシュ付きも救済したくなった場合だけ、話数の正規表現を `\/?$` に変更してください。

### A2. 転送ループ：✅ OK／実環境の追加転送は要確認

今回の転送先は、すべて `movedTo()` の条件から外れます。**この差分内ではループしません。**

`auto-trailing-slash` は、たとえば `foo.html` なら `/foo`、`foo/index.html` なら `/foo/` を正規形とします。そのため、成果物によっては次の追加転送が起きますが、ループではありません。[Cloudflare公式資料](https://developers.cloudflare.com/workers/static-assets/routing/advanced/html-handling/)

```text
/manga → /manga-archive → /manga-archive/ → 200
```

公開前には、実アセットを使って各転送先の最終応答が200になることを確認してください。正規形がスラッシュ付きなら、最初からそこへ転送すると1回で済みます。別途 `_redirects` や Cloudflare のルールがある場合、その逆向き転送も確認対象です。

### A3. 存在しない話への301：⚠️ 要注意・公開前の修正推奨

該当行は次です。

```js
const m = path.match(/^\/(manga|english-4-koma)\/ep([1-9])$/);
```

これは、ページの存在に関係なく両言語の1〜9話を転送します。

`301 → 404` はプロトコル上のエラーではありません。ただし、**「転送前と結果は同じ」なのは最終的に404になる点だけ**です。通信が1回増え、存在しない住所への恒久転送を記憶させる可能性があります。今回の「行き先がはっきりしているものだけ」という方針にも合いません。

**存在する話の番号だけを許可する修正を推奨します。** 8話分・6話分という件数だけでは連番とは断定できないため、実際の番号を列挙するのが確実です。

```js
if (m && EXISTING_EPISODES[m[1]].has(m[2])) {
  return `/${m[1]}/ep0${m[2]}`;
}
```

`EXISTING_EPISODES` は実在する番号の集合として定義します。毎リクエスト `env.ASSETS.fetch()` で存在確認する必要はなく、静的サイトなら明示リストかビルド時生成で十分です。

### B1. 既存処理への影響：⚠️ 基本は維持、記録とヘッダーに差分あり

- **`/api/`・`/media/`：✅ OK**  
  先に既存の `route()` へ返すため、今回の判定は通りません。
- **`/pixelstomotors/`：✅ OK**  
  旧形式の転送が先に動きます。たとえば `/pixelstomotors/manga` は `/manga` を経由して `/manga-archive` へ進む複数段転送になりますが、既存動作を壊してはいません。
- **静的アセット：✅ OK**  
  対象外のパスは、従来どおり元の `request` を `env.ASSETS.fetch()` に渡します。
- **クローラー記録：⚠️ 意図した観測ができなくなる**  
  元の住所へのアクセスは記録されません。追従先へのアクセスは記録され得ますが、どの旧住所から来たかは分からなくなります。配信上の問題はありませんが、今回の改善効果を測りたいなら、旧パス・転送先・301を記録する価値があります。記録失敗で転送を止めない方針は維持してください。
- **レスポンスヘッダー：⚠️ 確認対象**  
  新しい `Response.redirect()` は `secured()` を通りません。既存の静的レスポンス側でも共通ヘッダーを付けているなら、新しい301にはそのヘッダーが付きません。`secured()` の中身が未提示なので、必要なものがあるか確認してください。

### B2. 301の選択：✅ OK／戻す際は要注意

確認済みの旧住所から対応する現行ページへの移転なので、301で適切です。Googleも恒久転送を、転送先を正規URLとする強いシグナルとして扱います。[Google公式資料](https://developers.google.com/search/docs/crawling-indexing/301-redirects)

ただし301はキャッシュされ得ます。コードを戻しても、ブラウザが旧住所を再取得せず転送を続ける場合があります。検索エンジン側の評価更新にも再クロールが必要です。[HTTP仕様](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.2)

戻す可能性が具体的にある住所だけは302を検討できますが、今回の確定した移転をすべて302にする必要はありません。存在しない話を除外してから301を出すのが適切です。

### C1. その他の住所の判断：✅ OK／追加は根拠があるものだけ

`/ai-agent` や `/files/LOG395/{c}` を404のままにする判断は妥当です。対応する内容がない住所を一覧やトップページへ一括転送する必要はありません。

追加候補は `/record/precision-is-not-learning/` です。現コードでは末尾スラッシュ付きは対象外なので、アクセス実績があるか、旧ページの住所として使っていた根拠があれば完全一致表に追加してください。

大文字小文字・二重スラッシュ・任意の番号などへ、推測だけで条件を広げる必要はありません。

### 公開してよいか

**存在しない話への転送を除外し、実アセットで転送先の200を確認できれば、公開してよいと判断します。**

公開前に対応する項目は次のとおりです。

1. **修正推奨：** 話数の転送を、実在する番号に限定する。
2. **確認必須：** 実アセットで末尾スラッシュ処理を含む転送全体を追い、最終200・ループなしを確認する。
3. **運用判断：** 旧住所のアクセス記録を残すか、301にも共通ヘッダーが必要かを決める。

完全一致表と `/prototype1〜6` の正規表現には、提示情報の範囲で公開を妨げる問題はありません。
````

### codex の指摘を反映して公開した

指摘はどれも反映しました。

| 指摘 | 直したこと |
|---|---|
| A3 ページのない話まで 301 にしている | 1桁の話数は、実際にページがある話（4コマは1〜7・9、英語版は1〜4・7・9）だけ転送する |
| B1 転送した訪問がクローラーの記録に残らない | 転送した訪問も、結果 301 として記録する |
| B1 301 に共通ヘッダーが付かない | ほかの返事と同じ secured() を通す |
| C1 末尾に / が付いた形 | /record/precision-is-not-learning/ も転送する |
| A2 本番で最後に 200 が返るか | 公開後に確かめた（下） |

#### worker/index.js の変更（転送）

```diff
diff --git a/worker/index.js b/worker/index.js
index 2c44687..b4375c2 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -26,6 +26,18 @@ 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;
+      // クローラーの記録にも残す（返した結果 301。どの古い住所に来たかが分かるように）
+      try {
+        const bot = crawlerOf(request, url);
+        if (bot) ctx.waitUntil(crRecord(request, env, url, bot, 301).catch(() => {}));
+      } catch { /* 数え損ねても転送はする */ }
+      return secured(Response.redirect(to.toString(), 301));
+    }
     const res = await env.ASSETS.fetch(request);
     // 検索エンジン・AI のクローラーが読みに来たか（記録の失敗でページを止めない）
     try {
@@ -40,6 +52,26 @@ 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',
+  '/record/precision-is-not-learning/': '/record/precision-is-not-learning-so101',
+};
+// 1桁の話数で来たとき、0を付けた住所へ転送するのは、ページがある話だけ（ない話を転送しても 404 になるだけなので。2026-10-30 の codex のレビュー）
+const EPISODES_1_9 = { manga: new Set(['1', '2', '3', '4', '5', '6', '7', '9']), 'english-4-koma': new Set(['1', '2', '3', '4', '7', '9']) };
+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 && EPISODES_1_9[m[1]].has(m[2])) 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/')) {
```

公開後に、本番で古い住所を開いた結果です。かっこの中は転送の回数です。

```text
/manga → 200 https://pixelstomotors.com/manga-archive (1)
/manga/ep9 → 200 https://pixelstomotors.com/manga/ep09 (1)
/english-4-koma/ep7 → 200 https://pixelstomotors.com/english-4-koma/ep07 (1)
/record/precision-is-not-learning → 200 https://pixelstomotors.com/record/precision-is-not-learning-so101 (1)
/record/precision-is-not-learning/ → 200 https://pixelstomotors.com/record/precision-is-not-learning-so101 (1)
/prototype1 → 200 https://pixelstomotors.com/series1 (1)
/prototype6/ → 200 https://pixelstomotors.com/series6 (1)
/manga/ep8 → 404 https://pixelstomotors.com/manga/ep8 (0)
/ai-agent → 404 https://pixelstomotors.com/ai-agent (0)
```

どれも転送1回で目的のページに着き、ページのない話と /ai-agent は 404 のままです。



## 作業ログ ― SNS の予約の枠を広げ、課題地図の42ページを毎日出すようにした

### 記事の X の予約に、毎日7:30の枠を足した

この記事の X の予約は、決まりの空いている枠（平日12:30・平日19:30・土日12:30）がすでに埋まっていて、11/25 19:30 になりました。記事を出す速さが上がり、予約がどんどん先になっていたためです。

> LOGは前よりスピードが合って増えてるので、投稿時間を朝と昼にも振り分けていいよ（そうじゃないとどんどん先になるため）

昼の12:30 は平日・土日とも枠にあったので、朝の7:30 を毎日の枠に足しました。この記事の X は 10/31 7:30 に入れ直しています。

| | 前 | 後 |
|---|---|---|
| 平日 | 12:30・19:30 | 7:30・12:30・19:30 |
| 土日 | 12:30 | 7:30・12:30 |

決まりは、記事のルール（article_rules.md）と、予約投稿の手順書（r2r2r-content の docs/sns_autopost.md）に書きました。マンガの英語の投稿（平日22:30）と LinkedIn の研究の記録（平日17:00）の時刻は変えていません。

### 課題地図の42ページを、X は毎日21:00、LinkedIn は毎日8:00 に出す

課題地図（pixelstomotors.com/issues ）は、フィジカルAIが社会で使われる前に解くべき課題を、1課題1ページでまとめたページです。今は42ページあります。

> それと追加で。フィジカルAIが社会実装される前に解くべき課題。サイトのページごとに投稿するようにしてほしい（かなり数があるけど大事な情報なので発信する必要がある）

> linkdeinにも投稿して

記事の枠に入れると、42本で記事の予約が1か月以上先へ押し出されます。そのため、課題地図だけの枠を別に作りました。

| | 時刻 | 期間 | 文の形 |
|---|---|---|---|
| X | 毎日21:00 | 10/31〜12/11 | 1行目で手が止まる言い方・何が問題か・最初の一歩・URL・話題のタグ2つ |
| LinkedIn | 毎日8:00（X と同じ日） | 10/31〜12/11 | 1行目・この課題の要点3つ・地図にまとめていることの1文・読み手への問い・タグ。リンクはカードで付く |

出す順番は、課題ごとの「急ぎ度」と「深刻度」を掛けた点数の高い順です（同じ点数なら番号順）。最初は「介護現場へのロボット導入の壁」、次が「実機データの圧倒的不足」「動作のポータビリティ不在」です。文は r2r2r-content の `sns/ISSUE_{ページ名}_sns_posts.json` に1ページ1つずつ置き、42本とも予約の点検（`python3 tools/sns_autopost.py check`）を通し、リンク先の42ページがすべて開けることを確かめました。課題の数はこれから増えるので、文の中に数は書いていません。

- ねらい：課題地図を、検索や AI 経由だけでなく、SNS からも毎日見てもらう
- 変わったこと：課題地図のページが、X と LinkedIn に毎日1本ずつ出る。記事の予約は押し出されない
- 見込める効果（見込み）：課題地図（/issues）への人の表示が増える。今週は6回で、開いた人はだれもページの4分の1より先まで読んでいなかった。2週間後に、/issues/ の各ページの表示と、来た元の t.co（X）・linkedin.com の数を比べて確かめる

LinkedIn は、平日だと研究の記録（17:00）と課題地図（8:00）の2本が同じ日に出ます。前に LinkedIn の分析で、同じ日の投稿どうしが見られる数を取り合っていたので、2週間ほどで1本あたりの数字が下がっていないかも見ます。

## 次は、AI が読んだあとに人が来ているかを見る

今週の数字で分かったのは、AI は読みに来ているが、そこから人が来ているかはまだ見えていない、ということです。印の記録を入れたので、来週は2つの数字を見ます。

- 「どこから来たか」の表に、chatgpt.com・perplexity.ai などの印や来た元が出てくるか
- ChatGPT-User が読んだページが増えていくか。どのページが読まれているか

クローラー全体の回数は、どのサイトにも来る巡回が大半なので、方針の判断には使いません。
