---
title: "記録が検索で1位に出た ― 次はAIの回答の出典へ"
slug: first-search-hit-to-ai-citation
date: 2026-10-25
day: 385
meta_description: "「サービスロボット 導入 シミュレーション」のGoogle検索で、記録が大手より上の1位に出た。見出しと説明文を直し、人と機械の閲覧・AIのクローラーの訪問を数える仕組みとllms.txtを置き、Bingにも登録した記録。"
tags: [SEO, 検索, AI検索, llms.txt, Bing Webmaster Tools, Cloudflare Workers, アクセス解析, クローラー, Astro, サービスロボット]
series: "ひとり研究所の裏側"
---

Google で「サービスロボット 導入 シミュレーション」と検索すると、このサイトの記録が1位に出ていました。ログインしていない状態（シークレットウィンドウ）でも同じで、ソフトバンクロボティクスや富士ソフトのページより上です。画像の検索では、上の3枚がこのサイトの図でした。

このサイトの記事は、いわゆる検索向けの記事ではありません。作業ログ（その日にやったことを、コードと実行の記録まで含めて残した記事）が380本以上並んでいて、題名も「詰まりの元は、ロボットを線に縛った作りだった」のような書き方です。それでも検索で見つかったのは、配膳ロボットの導入前の試算という、中身のあるページを出している所がほとんどない分野だったからだと考えています。

一方で、検索結果の上に出る「AI による概要」（Google の AI が検索結果をまとめた文）の出典には、このサイトは入っていませんでした。作業ログは、もともと人より AI が読む前提で作っています。コードと記録を全部載せ、AI が読み取りやすい形の全文も添えています。

> LOGはそのためにつくったので、うまく表示されるようになるといいですが。

そこで、検索で見つかった入口のページを直し、AI に拾われているかを数えられるようにしました。やったことは次の6つです。

| やったこと | なぜ | 結果 |
|---|---|---|
| 検索結果を、ログインあり・なしで比べた | 自分の閲覧の履歴で上に出ているだけかもしれないため | ログインなしでも通常の検索で1位、画像で上位3枚 |
| 全ページの見出しを「日本語 \| 英語」の順にした | Google が検索結果の題名を、英語から始まる見出しに置き換えていたため | 全8シリーズ・トップ・記事・4コマ・解説動画の見出しを公開 |
| 配膳ロボットのシリーズのページに、言葉の説明と一覧を足した | AI による概要は、言い切る説明文と箇条書きを拾うため | 「〜とは、〜を試算して確かめることです」から始まる形に |
| 閲覧の記録に、人か機械かの印を付けた | 1週間の表示1,270回の大半が、人の閲覧ではなさそうだったため | 人・機械・操作なしを分けて数えられるように |
| AI のクローラー（ページを読みに来るプログラム）の訪問を数えた | ページ上の計測では、AI のクローラーが見えないため | GPTBot・ClaudeBot など28種類を、どのページを読んだかまで記録 |
| AI 向けの案内（llms.txt）を置き、Bing に登録した | ChatGPT の検索は Bing の検索結果を使うため | 案内は公開、Bing はサイトマップの処理待ち |

**検索で見つかるかどうかは、入口のページの言葉で決まっていました。作業ログそのものは記録のままにして、シリーズのページのような入口だけを検索の言葉に合わせる、という分け方にしています。**

人が検索から来た数は、まだ週に100回ほどです。月に何万回という伸び方をする分野ではありません。ただ、配膳ロボットを入れるか迷っている店の人や、ロボットを売る会社の人が、実際に打ち込む言葉で大手と並んで出ています。次は、AI の回答の出典として使われているかを、クローラーの記録と Bing の「AI Performance」（Bing の AI の回答で出典に使われた回数を見る画面）で確かめます。


## 作業ログ ― 検索で1位を確かめて、AI に拾われているかを数えられるようにする

ここから下は作業の記録です。効果のうち、検索の順位と閲覧の数は実際に見た値です。見出しや説明文を直した効果は、まだ測っていない見込みで、何の数字で確かめるかを添えています。

### ログインした検索で、画像の枠の6枚中5枚がこのサイトだった

最初に気づいたのは、ログインしたままの Chrome で「サービスロボット シミュレーション」と検索したときです。右側の画像の枠の6枚のうち5枚が、サービスロボット導入シミュレーションのシリーズの図でした。AI による概要の横に出る画像も、Log#12 の「GIVE THE ROBOT A MAP.」の図です。

【画像】LOG385_01_google_logged_in.jpg ログインした状態で「サービスロボット シミュレーション」を検索した画面。右の画像の枠に Pixels to Motors の図が5枚並び、AI による概要の横の画像も同じシリーズの図

ただ、Google はログインしている人の閲覧の履歴で順番を変えます。自分のサイトを毎日開いていれば、自分にだけ上に出ることがあります。そのため、ログインしていない状態で同じ検索をすることにしました。

### シークレットウィンドウでも、画像の検索に2枚出ていた

シークレットウィンドウ（閲覧の履歴を使わない Chrome の窓）で同じ言葉を検索し、画像のタブを開きました。十数枚のうち2枚がこのサイトの図で、「2日前」「4日前」と公開の新しさが表示されていました。

【画像】LOG385_02_google_incognito_images.jpg シークレットウィンドウで「サービスロボット シミュレーション」の画像を検索した画面。左の列の2段目「WHERE THE ROBOTS JAM.」と、4列目の下の店の平面図がこのサイトの図

ログインしていたときの「6枚中5枚」には、閲覧の履歴の分が乗っていたことになります。それでも、履歴なしで企業のページと同じ並びに入っていました。

### 「サービスロボット 導入 シミュレーション」では、通常の検索で1位だった

言葉に「導入」を足して、シークレットウィンドウのまま検索しました。通常の検索結果（青い題名の一覧）の1番目が、pixelstomotors.com/series8（サービスロボット導入シミュレーションのシリーズのページ）でした。その下に、ソフトバンクロボティクスの配膳ロボット Servi の簡単シミュレーション、Gravio の Artefacts、富士ソフトの複数台同時シミュレーションが続きます。

【画像】LOG385_03_google_rank1.jpg シークレットウィンドウで「サービスロボット 導入 シミュレーション」を検索した画面。AI による概要の下、通常の検索結果の1番目が pixelstomotors.com の series8

【画像】LOG385_04_google_rank1_list.jpg 同じ検索結果を下へ送った画面。1位の series8 の下に、ソフトバンクロボティクス・Gravio・富士ソフトのページが続く

画像のタブでは、上の3枚が3枚ともこのサイトの図でした（「19時間前」「4日前」「2日前」）。

【画像】LOG385_05_google_images_top3.jpg 同じ言葉の画像の検索。上の段の左から3枚が Pixels to Motors のサービスロボット導入シミュレーションの図

「19時間前」「2日前」のように公開の新しさが表示されているので、公開したばかりのページを一時的に上に出す働き（鮮度の効果）が乗っている可能性があります。定着したかどうかは、1〜2週間後に同じ検索で確かめます。

### AI による概要の出典は Gravio・富士ソフト・SF Twin で、1位のページは入っていなかった

同じ検索の上に出る AI による概要を開くと、出典として並んでいたのは Gravio、富士ソフト、SF Twin などでした。通常の検索で1位の series8 は入っていません。

【画像】LOG385_06_ai_overview.jpg AI による概要を開いた画面。「シミュレーションで確認できる主な項目」「代表的なシミュレーション方法・ツール」の見出しが並び、右の出典は Gravio と富士ソフト

AI による概要の文は、「〜は、〜するプロセスです」という言い切りの一文と、「動線・レイアウト検証」「作業効率」「費用対効果」のような見出し付きの箇条書きでできていました。出典に選ばれているページも、同じ形の文を持っています。series8 の説明文は「〜を、導入前に試算する記録です」で、記録の紹介にはなっていても、言葉の説明にはなっていませんでした。

「代表的なシミュレーション方法・ツール」の項目には「Web上の簡易ROI・効果シミュレーション」がありました。series8 の中には、ブラウザで動く試算が6つの現場ぶん入っています。中身は当てはまるのに、本文でそう書いていなかったことになります。

### series8 の検索結果の題名は、英語から始まる見出しに置き換えられていた

検索結果の題名は「Service Robot Simulation | サービスロボット導入 ...」でした。ところが、ページの title（ブラウザのタブに出る題名）は日本語から始まっています。

| どこ | 中身 |
|---|---|
| ページの title | サービスロボット導入シミュレーション（Service Robot Simulation）\| Pixels to Motors |
| ページの見出し（H1） | Service Robot Simulation \| サービスロボット導入シミュレーション |
| 検索結果に出た題名 | Service Robot Simulation \| サービスロボット導入 ... |

Google は、title より見出しのほうがページの中身を表していると判断すると、検索結果の題名を見出しに置き換えます。日本語で検索した人に英語から始まる題名を見せていたので、題名の頭で検索した言葉と合っていることが伝わりにくくなっていました。

見出しの「英語 | 日本語」の並びは、series8 だけでなく全8シリーズのページと、トップの「Record | 記録」などで同じ作りです。series8 だけを変えると、ほかのシリーズと並びがそろわなくなります。そこで、直す前に2つを決めてもらいました。

| 決めること | 選んだもの |
|---|---|
| 見出しの並び | 全シリーズで日本語を先にする（「サービスロボット導入シミュレーション \| Service Robot Simulation」） |
| series8 の説明文 | 言葉の説明の一文から始め、「分かること」「試算の方法」の一覧も足す |

### series8 の説明文を、言葉の説明の一文から始めた

説明文は、シリーズの設定を書いているファイル（src/lib/records.ts）の series8 の intro にあります。この文はページの説明（検索結果の題名の下に出る説明文）にも使われています。

| | 説明文の書き出し |
|---|---|
| 前 | 配膳・清掃・搬送など、店や施設で動くサービスロボットを入れたら何が変わるかを、導入前に試算する記録です。 |
| 後 | サービスロボット導入シミュレーションとは、配膳・清掃・搬送などのロボットを店や施設に入れる前に、待ち時間・必要な人数・ロボットが止まる場所・費用を試算して確かめることです。 |

その下に、見出し付きの一覧を2つ置きました。画面では横に2列、スマホでは縦に並びます。見出しは H2 にしたので、ページの構造としても「このページには何が分かるか・どう試算するかが書いてある」と読めます。

- シミュレーションで分かること：必要な人数、待ち時間、止まる場所、費用
- 試算の方法：離散イベントシミュレーション（SimPy）、3Dシミュレーション（Unity）、前提の数字（公開事例と観察シート）、ブラウザで試せる6つの現場の試算

【画像】LOG385_13_series8_after.jpg 直したあとの series8 の上の部分。見出しが「サービスロボット導入シミュレーション | Service Robot Simulation」になり、「〜とは」から始まる説明文の下に「シミュレーションで分かること」「試算の方法」の2列の一覧が並ぶ

- ねらい：AI による概要が拾う形（言い切る説明文と、見出し付きの箇条書き）を、検索で1位のページの本文に置く
- 変わったこと：説明文が「〜とは、〜を試算して確かめることです」で始まり、分かること4つと試算の方法4つが本文の文字で読める。見出しも日本語が先になった
- 見込める効果：検索結果の題名が「サービスロボット導入シミュレーション | …」になり、AI による概要の出典に入る見込み。題名は Google が読み直したあとの検索結果で、出典は同じ検索の AI による概要で確かめる（まだ測っていない）

公開する前に、手元でサイトを作り直し（ビルド）、パソコンとスマホの幅で画面を撮って崩れていないかを見ました。公開したあと、本番のページの見出しも確かめています。

### 全ページの見出しを日本語先にそろえた

シリーズのページを直したあと、ほかのページもそろえることにしました。「英語 | 日本語」の並びの見出しを、ソースの中から探して入れ替えています。

| ページ | 前 | 後 |
|---|---|---|
| 全8シリーズのページ（見出し） | Service Robot Simulation \| サービスロボット導入シミュレーション など | サービスロボット導入シミュレーション \| Service Robot Simulation など |
| 全8シリーズのページ（試算・記録） | Simulator \| 試算を試す／Record \| 記録 | 試算を試す \| Simulator／記録 \| Record |
| トップ | Year One／Guide／Archive／Thinking／Series／Record／Sole Prop が先 | Pixels to Motors の1年／導入前ガイド／4コママンガ／いまの考え／シリーズ／記録／事業所について が先 |
| トップの解説動画 | Video \| 解説動画 | 解説動画 \| Video |
| 記事ページの下 | Series \| ◯◯の記録 | ◯◯の記録 \| Series |
| 4コマの一覧 | Archive \| 4コママンガ／Archive \| English 4-Koma | 4コママンガ \| Archive／English 4-Koma \| Archive |
| 4コマの各話の下 | Related Records \| マンガの背景にある記録 | マンガの背景にある記録 \| Related Records |
| メッセージ・プライバシーポリシー | Message \| メッセージ／Privacy policy \| プライバシーポリシー | メッセージ \| Message／プライバシーポリシー \| Privacy policy |

シリーズのページは、設定のファイルの題名（「英語 | 日本語」）を書き換えていません。題名を「 | 」で分けて、表示するときに順番を入れ替えています。設定の題名は、記事ページの「◯◯の記録」など別の所でも分けて使っているためです。

英語版の4コマには日本語の名前がないので、「English 4-Koma | Archive」と名前を先にして、並びの形だけそろえました。トップの一番上の「Pixels to Motors | 人とロボットの関わりを…」は、英語と日本語の言い換えではなく、ブランドの名前とキャッチコピーの組なので変えていません。

トップの解説動画の見出しは、最初の検索では見つかりませんでした。トップに埋め込んでいる別の HTML（public/embeds/top-video.html）の中にあったためです。画面写真をもらってから、埋め込みの HTML も含めて探し直しています。

【画像】LOG385_07_top_video_heading.jpg 直す前のトップの解説動画の段。見出しが「Video | 解説動画」のまま残っていた

### 1週間の表示1,270回のうち、人の閲覧は検索とSNSから来た約100回と見た

検索から人が来ているかを見るため、サイトの使われ方の記録（ページ上の小さなスクリプトが、1回の表示ごとに読んだ深さや押したものを送る仕組み）を集計しました。集計のページは鍵がないと開けないので、鍵を置いている外付けの SSD がつながった Mac の上で取り出しています。

| 10月19日〜25日の週 | 数 |
|---|---|
| 表示 | 1,270回（前の週は22回） |
| どこから来たか不明 | 1,135回 |
| Google から | 93回 |
| Yahoo! から | 6回 |
| LinkedIn から | 4回 |
| 表示時間の中央値 | 6秒 |
| ページの75%まで読んだ割合 | 82% |
| スマホからの割合 | 4% |
| 画面の下を走るハコブくんが出た／押された | 907回／0回 |
| メッセージのページの表示／フォームの送信 | 6回／0件 |

1,270回の大半は、人の閲覧ではないと見ました。理由は4つです。どこから来たか分からない表示が9割、表示時間の中央値が6秒なのに8割がページの75%まで読んでいる（人の読み方としては速すぎる）、スマホが4%しかない、ハコブくんが907回出て一度も押されていない。サイトを移した10月17日の直後から急に増えているので、検索エンジンや AI のプログラムがサイトを読み直している、と考えるのが自然です。

検索とSNSから来た約100回は、参照元（どのサイトから来たか）が残っています。自動で巡回するプログラムはふつう参照元を残さないので、こちらはほぼ人の閲覧と見ています。人の閲覧の動きとしては、トップ（126回・トップの動画の再生45回）が入口として一番働いていて、古い記録（DAY18・17・126・19 など）も今も読まれていました。DAY18 は表示時間の中央値が82秒です。

検索からの流入については、こう聞かれました。

> seoをすごい意識してつくっていないですが（LOGが多くていわゆるseo記事ではないので）少しは検索からの流入数も見込めそうですかね。

細かい言葉で探す人には、実測値とコードが載った作業ログがそのまま答えになります。1本あたりの流入は少なくても、本数が多いので積み上がります。ただ、検索する人の数そのものが少ない分野なので、来るのは週に数十〜数百人の規模と見ています。作業ログを検索向けの記事に作り変えるのではなく、入口のページに検索の言葉と説明を置く、という分け方にしました。

### 閲覧の記録に、人の操作と自動のブラウザの印を足した

人と機械を分けて数えないと、来週以降の数字も同じように混ざります。印を足すことにしました。

> これはしておいた方が良さそうだね（うちは個人のサイトの割に更新頻度が多いはずなのでクローラーが回ってきているのも多そう）

1回の表示ごとに、次の3つの印を付けます。

| 印 | 付く条件 | どこで見るか |
|---|---|---|
| 人（h） | マウスを動かす・押す・指で触れる・ホイール・キーのどれかがあった | ページ上のスクリプト |
| 機械（b） | ブラウザの名乗り（ユーザーエージェント）に bot・crawl・headless などがある、プログラムで動かしているブラウザ（navigator.webdriver）、Cloudflare が本物の bot と確かめた | ページ上のスクリプトとサーバー |
| 操作なし（x） | どちらでもない（開いてすぐ閉じた人も入る） | ― |

印を付ける前の記録は「印なし」として残し、あとから分けることはしません。集計のページは、週ごとの表に4つの内訳の列を足し、`?who=human` を付けると、ページごと・どこから来たか・押したものの表を人の閲覧だけに絞れるようにしました。

名乗りは判定に使うだけで保存しません。それでも記録する内容が増えたので、プライバシーポリシーの「サイトの使われ方の記録について」に、人による閲覧か自動のプログラムかの判定結果を記録すること、名乗りそのものは保存しないことを書き足しました。

判定の決め方を、いくつかの名乗りで確かめた出力です。

```
h Mozilla/5.0 (Macintosh) Chrome/130 Safari/537.36 {"u":1}
x Mozilla/5.0 (Macintosh) Chrome/130 Safari/537.36 {"u":0}
b Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.c {"u":0}
b Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatibl {"u":1}
b Mozilla/5.0 HeadlessChrome/130 {"u":1}
h Mozilla/5.0 (Linux; Android 12; Cubot X50) Chrome/130 Mobile {"u":1}
b Mozilla/5.0 Chrome/130 {"u":1,"wd":1}
b  {"u":1}
```

「Cubot」はスマホの機種名です。最初の決め方（bot の後ろが単語の区切り）だと「Cubot X50」の「bot 」に当たってしまうので、「bot/」「bot;」と、前後が区切りの「bot」だけを当てるようにしました。

手元でサイトを動かし、ページから送られる中身も確かめています。自動のブラウザで開いただけの表示（plain）には機械の印が、マウスを動かした表示（mouse）には人の操作の印が付きました。mouse でも wd が 1 なのは、試しに使った自動のブラウザ自身が navigator.webdriver を立てているためで、サーバー側では機械に数えられます。

```
plain {}
plain {"p":"/series8","u":0,"wd":1,"hl":1}
mouse {}
mouse {"p":"/series8","u":1,"wd":1,"hl":0}
```

公開したあと、本番の集計に4つの列が増えたことを確かめました。この時点の記録はすべて「印なし」です。

```
None {'humanViews': 0, 'botViews': 0, 'unknownViews': 0, 'unmarkedViews': 1270}
who human counted 0
人による閲覧か
```

- ねらい：人の閲覧と、検索エンジン・AI のプログラムの表示を分けて、検索から人が来ているかを正しく見る
- 変わったこと：週ごとの表に「人・機械・操作なし・印なし」の列が増え、`?who=human` で人の閲覧だけの表が見られる
- 見込める効果：来週の集計から、人の閲覧の数と、人がどのページを読んだかが混ざらずに見える（来週の集計で確かめる）

### AI のクローラーは JavaScript を動かさないので、配信のときに名乗りで数える

人と機械の印を足したあとで、足りないことに気づきました。ページ上の計測は、ページの中のスクリプト（JavaScript）が動いて初めて記録を送ります。GPTBot や ClaudeBot のような AI のクローラーの多くは、ページを文字として読むだけでスクリプトを動かしません。つまり、作業ログが AI に読まれているかは、ページ上の計測では見えません。

そこで、ページを配信するときに数えることにしました。このサイトは、ページを作り置きの静的なファイルとして Cloudflare から配信していて、画像・メッセージフォーム・集計など一部の住所だけを、Cloudflare Workers（配信の前にコードを動かせる仕組み）の worker/index.js が受け持っていました。配信の設定（wrangler.jsonc の run_worker_first）を変えて、ページとテキストも worker を通すようにしています。

| 住所 | 前 | 後 |
|---|---|---|
| /media/・/api/・旧サイトの住所 | worker を通す | worker を通す |
| ページ（/record/…・/series8 など）・llms.txt・記事の全文の Markdown | 静的ファイルを直接返す | worker を通し、クローラーなら記録する |
| 画像・CSS・JS・図解・試算の部品 | 静的ファイルを直接返す | 静的ファイルを直接返す（数えない） |

【図解】LOG385_who_is_counted.html

人がマウスを動かして読むと閲覧の記録に「人」の印が残り、GPTBot が読みに来るとクローラーの記録にだけ残ります。上の図で、開く人・プログラムを切り替えると、どこに記録が残るかが変わります。

名乗りで見分けるクローラーは28種類です。

| 種類 | クローラー |
|---|---|
| AI の学習・検索・利用者の依頼 | GPTBot・OAI-SearchBot・ChatGPT-User・ClaudeBot・Claude-SearchBot・Claude-User・anthropic-ai・PerplexityBot・Perplexity-User・Google-CloudVertexBot・MistralAI-User・cohere-ai・YouBot・DuckAssistBot・meta-externalagent・Bytespider・CCBot・Amazonbot・Applebot・Diffbot・FacebookBot |
| 検索エンジン | Googlebot・GoogleOther・bingbot・DuckDuckBot・YandexBot・Baiduspider・Yeti |

記録に残すのは、どのクローラーか・どのページか・返した結果（200 など）・Cloudflare が本物と確かめたか・日時だけです。名乗りはまねできるので、「Cloudflare が確かめたか」を一緒に残しています。集計は `/api/cr-stats` で、クローラーごと（読んだ回数・読んだページの数・作業ログの数・最後に来た日）、週ごと、よく読まれたページを見られます。

公開したあと、ページ・画像・記事の全文・旧サイトからの転送が今までどおり返ることを確かめました。

```
200 /
200 /series8
200 /record/serving-robot-map-planner
200 /llms.txt
200 /files/LOG382/LOG382_full.md
200 /site/pixelstomotors-logo.svg
200 /robots.txt
301 /pixelstomotors/series8
# Pixels to Motors

spoof 200
```

最後の spoof は、Naver の検索のクローラー（Yeti）の名乗りで /message を開いた確かめです。集計に1件入り、Cloudflare が本物と確かめられなかったことも記録されました。この1件は試しに送ったものです。

```
total 1
{'bot': 'Yeti', 'who': 'Naver（検索）', 'visits': 1, 'pages': 1, 'records': 0, 'verified': 0, 'unverified': 1, 'errors': 0, 'last': '2026-10-25'}
[{'path': '/message', 'visits': 1, 'bots': 'Yeti'}]
```

サイトの巡回の決まり（robots.txt）は、すべてのプログラムに全ページを許可しています。GPTBot・ClaudeBot・PerplexityBot・OAI-SearchBot・Googlebot の名乗りで記事を開くと、どれも 200（表示できた）が返りました。ただし、Cloudflare には、本物の AI のクローラーを送り元で見分けて止める設定（AI Crawl Control）があります。名乗りをまねた確かめでは、その設定までは試せません。管理画面で、止める設定になっていないかを見てもらうことにしました。

```
User-agent: *
Allow: /

Sitemap: https://pixelstomotors.com/sitemap-index.xml
---
llms 404
GPTBot/1.1 200
ClaudeBot/1.0 200
PerplexityBot/1.0 200
OAI-SearchBot/1.0 200
Googlebot/2.1 200
```

- ねらい：作業ログが、どの AI に、どのくらい読まれているかを数字で見る
- 変わったこと：AI のクローラーと検索エンジンが読みに来るたびに、どのページを読んだかが記録され、`/api/cr-stats` で見られる
- 見込める効果：数日たてば、AI のクローラーが作業ログを読んでいるかが分かる。数日たっても AI のクローラーが0件なら、Cloudflare の AI Crawl Control で止めている可能性が高い（数日後の集計で確かめる）

### llms.txt を、ビルドのたびにシリーズと記録の一覧から作る

llms.txt は、AI がサイトを読むときの案内として提案されている形式のファイルです。主要な AI が公式に読むとは言っていないので、効果ははっきりしません。作るのは軽いので置くことにしました。

中身は、サイトの説明、記録の書き方（コードとログを全部載せていること、試算の前提の数字は仮の値であること）、8シリーズ、ガイドのページ、記録の一覧です。長いコードとログを別のファイルに分けた記事は、全文の Markdown の住所も添えています。手で書くと記事を出すたびに古くなるので、サイトを作るたびに、シリーズの設定と記録の一覧から作り直すようにしました（src/pages/llms.txt.ts）。作り直した時点で423行、そのうち全文の Markdown の住所が付いた記録は33本でした。

【画像】LOG385_14_llms_txt.jpg 公開した pixelstomotors.com/llms.txt の上の部分。サイトの説明と記録の書き方のあとに、シリーズの一覧が並ぶ

### Bing Webmaster Tools に pixelstomotors.com を登録した

ChatGPT の検索は、Bing の検索結果を使っています。Bing にサイトが載っていなければ、ChatGPT の回答の出典にはなりにくいので、Bing の管理画面（Bing Webmaster Tools）にサイトを登録しました。登録の操作は本人が行い、手順と、所有者の確認のファイルをサイトに置く作業をこちらで受け持っています。

最初の画面には、Google Search Console から取り込む方法と、手で追加する方法がありました。取り込む方を選んでもらいました。

【画像】LOG385_08_bing_import.jpg Bing Webmaster Tools の最初の画面。左が Google Search Console から取り込む方法、右が手で追加する方法

取り込まれたのは、移す前のサイト adaptdesign.jp でした。Search Console には新しい pixelstomotors.com が入っていなかったためです。前のサイトのデータは、3か月でクリック18回・表示（インプレッション）1,500回ほどで、8月にあった表示が9月以降ほぼ0になっていました。サイトを移したので、当然の動きです。

【画像】LOG385_09_bing_old_site.jpg 取り込まれた adaptdesign.jp の画面。クリック18回・インプレッション1.5K で、8月の山のあと9月以降はほぼ0

そこから pixelstomotors.com を手で追加し、所有者の確認に XML ファイルを選びました。ダウンロードした BingSiteAuth.xml を、Mac と連携しているフォルダに入れてもらい、こちらで中身を読んでサイトの一番上（public/BingSiteAuth.xml）に置いて公開しています。

【画像】LOG385_10_bing_verify.jpg 所有者の確認の方法を選ぶ画面。XML ファイル・HTML のメタタグ・DNS の CNAME の3つから XML ファイルを開いたところ

```
<?xml version="1.0"?>
<users>
	<user>3AB7818E9A5C06DB0AE2E883B8555FDB</user>
</users>

200 application/xml
```

確認が通り、左上が pixelstomotors.com になりました。続けてサイトマップ（https://pixelstomotors.com/sitemap-index.xml ）を送り、いまは「処理中」です。

【画像】LOG385_11_bing_home.jpg 確認が通ったあとの Bing Webmaster Tools のホーム。左上が pixelstomotors.com/ になり、データは最大48時間で反映されると出ている

【画像】LOG385_12_bing_sitemap.jpg サイトマップを送った画面。sitemap-index.xml が「処理中」で、検出された URL はまだ0

Bing Webmaster Tools の左のメニューには「AI Performance」（ベータ版）があります。Copilot や Bing の AI の回答で、このサイトのページが出典として使われた回数を見る画面です。作業ログが AI に拾われているかを、Google より先に数字で確かめられるので、1〜2週間後に見ます。

### コードを全部載せる方針と、載せないと決めた3つ

この記事を公開したあと、深津貴之さん（THE GUILD・note）の X の投稿が目に入りました。

> テックブログやXに技術スタックを書くこと自体が、脆弱性になる時代。情報の透明性と安全の確保をどうすべきか問い直されそうです。

このサイトの作業ログは、コードとログを全部載せています。この記事にも、配信の仕組み（worker/index.js）の全文と、集計ページの守り方が入っています。そこで聞かれました。

> 技術スタックの話だけど、実際に販売するサービスなどに使っているもののコードを掲載する危険性は高いけど、実際どうなんだろ？そんなに関係あるかな？

技術スタック（Astro・Cloudflare・Unity のような、使っている道具の名前）を書くこと自体の危険は、このサイトでは小さいと考えました。攻撃の大半は、プログラムがネット中を総当たりで調べる自動のもので、ブログを読んでから狙いを決めるわけではありません。使っている道具は、隠しても配信の返事などから外から分かります。守るものも、静的なページとメッセージフォームの中身くらいです。

危ないのは、書くことではなく、載せる中身のほうです。

| 危ないもの | このサイトでの扱い |
|---|---|
| 鍵（API キー・トークン・パスワード）がコードに混ざる | 鍵は KIOXIA に置き、コードは読み込むだけ。公開中のファイルを鍵の形の文字列で探し、出てきたのは「ghp_ のような文字列を探した」と説明した記事の文字だけだった |
| まだふさいでいない穴を書く | ふさいでから書く。LOG365（集計の鍵・送信の回数・AI の書き込み先・2段階認証の4つ）も、ふさいだあとに書いた |
| お客さんの案件・売る仕組みのコード | 載せない。載せるのは自分の研究とこのサイトのコードだけ |
| 守りの仕組みの細かい数字や名前（流量制限の回数・罠の欄の名前・ボットを見分ける文字の一覧） | 載せる。読めばすり抜け方は分かるが、すり抜けられても起きるのは迷惑な送信や集計のずれ程度 |

AI で、公開されたコードを読んで弱いところを探す手間はほとんどなくなりました。それでも、このサイトの場合はすり抜けて得をする人がほぼいません。全部載せることで残る「同じ手順で再現できる記録」「AI に読まれる記録」の価値のほうが大きい、と判断しました。

上の表の上の3つは、記事の決まり（article_rules.md）に「全量掲載でも載せないもの」として足しました。

- ねらい：コードを全部載せる方針はそのままに、載せてはいけないものだけを先に決めておく
- 変わったこと：鍵・ふさいでいない穴・お客さんの案件と売る仕組みのコードの3つを、記事・SNS・サイトのどこにも載せない決まりにした
- 見込める効果：記事を書くたびに「これは載せてよいか」で迷わずに済み、鍵の混入は公開前の文字列の検索で止められる（まだ測っていない）

### この日の公開（7回のコミット、p2m-site）

```
62b9110 2026-10-25 20:19 シリーズのページの見出しを日本語先に、series8 に定義の一文と一覧を追加
44586e3 2026-10-25 20:23 全ページの見出しを日本語先にそろえた（トップ・記事・4コマ・メッセージ・プライバシーポリシー）
c80d7b1 2026-10-25 20:26 トップの解説動画の見出しも日本語先に
7b50e3e 2026-10-25 20:33 サイトの使われ方の記録に、人と機械を分ける印を足した
5a5adaf 2026-10-25 20:34 README に人と機械を分ける印の説明を足した
e8ce84f 2026-10-25 20:42 llms.txt を置き、検索エンジン・AI のクローラーの訪問を数えるようにした
e76486e 2026-10-25 20:50 Bing Webmaster Tools の所有者確認のファイルを置いた
```

### 出力：閲覧の記録を Mac の上で取り出した（集計の鍵は KIOXIA から読み、画面に出していない）

```bash
cd $HOME; K=$(tr -d '\n\r ' < $HOME/mnt/KIOXIA/p2m_stats_key.txt); for p in ev-stats hk-stats; do curl -s -H "Authorization: Bearer $K" "https://pixelstomotors.com/api/$p?format=json" -o $p.json -w "$p %{http_code} %{size_download}\n"; done; python3 -c "
import json;d=json.load(open('ev-stats.json'));print(list(d.keys()) if isinstance(d,dict) else type(d))"
```

```
ev-stats 200 27667
hk-stats 200 3501
['total', 'truncated', 'since', 'week', 'byWeek', 'byPage', 'moves', 'referrers', 'clicks', 'areas', 'deadClicks']
```

### 出力：週ごと・ページごと・どこから来たか・押したもの（上位25件ずつ）

```python
import json
d=json.load(open('ev-stats.json'))
print('total',d['total'],'trunc',d['truncated'],'since',d['since'],'week',d['week'])
for k in ['byWeek','byPage','referrers','areas','deadClicks','moves','clicks']:
    v=d[k]
    print('==',k, type(v).__name__, len(v))
    items = v if isinstance(v,list) else list(v.items())
    for x in items[:25]: print(json.dumps(x,ensure_ascii=False)[:260])
```

```
total 1292 trunc False since None week None
== byWeek list 2
{"week": "2026-10-19", "label": "2026-10-19 〜 10-25", "views": 1270, "medianSeconds": 6, "read75Pct": 82, "spPct": 4, "backViews": 20, "outClicks": 0, "deadClicks": 69, "rageClicks": 1, "messageViews": 6, "formStarts": 0, "formSubmits": 0, "formSubmitsValid": 
{"week": "2026-10-12", "label": "2026-10-12 〜 10-18", "views": 22, "medianSeconds": 17, "read75Pct": 5, "spPct": 36, "backViews": 0, "outClicks": 0, "deadClicks": 1, "rageClicks": 0, "messageViews": 0, "formStarts": 0, "formSubmits": 0, "formSubmitsValid": 0, 
== byPage list 100
{"path": "/", "views": 126, "medianSeconds": 23, "read25Pct": 46, "read50Pct": 42, "read75Pct": 41, "read100Pct": 38, "deadClicks": 2, "rageClicks": 0, "copies": 0, "videoPlays": 45, "outClicks": 0}
{"path": "/privacypolicy", "views": 20, "medianSeconds": 2, "read25Pct": 30, "read50Pct": 15, "read75Pct": 15, "read100Pct": 15, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day18", "views": 17, "medianSeconds": 82, "read25Pct": 88, "read50Pct": 41, "read75Pct": 35, "read100Pct": 12, "deadClicks": 3, "rageClicks": 0, "copies": 1, "videoPlays": 0, "outClicks": 0}
{"path": "/record/building-a-physical-ai-lab-at-home", "views": 13, "medianSeconds": 15, "read25Pct": 69, "read50Pct": 62, "read75Pct": 62, "read100Pct": 23, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/issues", "views": 12, "medianSeconds": 12, "read25Pct": 17, "read50Pct": 8, "read75Pct": 8, "read100Pct": 0, "deadClicks": 1, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day17", "views": 11, "medianSeconds": 6, "read25Pct": 73, "read50Pct": 64, "read75Pct": 55, "read100Pct": 45, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/series6", "views": 11, "medianSeconds": 9, "read25Pct": 91, "read50Pct": 45, "read75Pct": 36, "read100Pct": 18, "deadClicks": 2, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day126", "views": 9, "medianSeconds": 28, "read25Pct": 100, "read50Pct": 78, "read75Pct": 56, "read100Pct": 33, "deadClicks": 12, "rageClicks": 1, "copies": 1, "videoPlays": 0, "outClicks": 0}
{"path": "/record/studio-to-astro-cloudflare", "views": 9, "medianSeconds": 9, "read25Pct": 0, "read50Pct": 0, "read75Pct": 0, "read100Pct": 0, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/series8", "views": 8, "medianSeconds": 16, "read25Pct": 25, "read50Pct": 25, "read75Pct": 13, "read100Pct": 0, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day19", "views": 7, "medianSeconds": 21, "read25Pct": 100, "read50Pct": 86, "read75Pct": 86, "read100Pct": 57, "deadClicks": 0, "rageClicks": 0, "copies": 1, "videoPlays": 0, "outClicks": 0}
{"path": "/record/japan-robot-leader-physical-ai-gap", "views": 6, "medianSeconds": 15, "read25Pct": 50, "read50Pct": 33, "read75Pct": 33, "read100Pct": 17, "deadClicks": 1, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/robot-ideas-from-pain-not-robots", "views": 6, "medianSeconds": 4, "read25Pct": 67, "read50Pct": 67, "read75Pct": 67, "read100Pct": 67, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/message", "views": 6, "medianSeconds": 12, "read25Pct": 100, "read50Pct": 100, "read75Pct": 83, "read100Pct": 0, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/four-layers-of-robot-softness", "views": 5, "medianSeconds": 7, "read25Pct": 40, "read50Pct": 40, "read75Pct": 40, "read100Pct": 40, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day145", "views": 5, "medianSeconds": 5, "read25Pct": 60, "read50Pct": 60, "read75Pct": 40, "read100Pct": 40, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/so101-imitation-learning-papers", "views": 4, "medianSeconds": 11, "read25Pct": 25, "read50Pct": 0, "read75Pct": 0, "read100Pct": 0, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day12", "views": 4, "medianSeconds": 11, "read25Pct": 100, "read50Pct": 100, "read75Pct": 100, "read100Pct": 75, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/robot-car-kit-autonomy-breakdown", "views": 4, "medianSeconds": 9, "read25Pct": 100, "read50Pct": 100, "read75Pct": 100, "read100Pct": 100, "deadClicks": 1, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/what-to-hand-to-robots", "views": 4, "medianSeconds": 7, "read25Pct": 50, "read50Pct": 50, "read75Pct": 25, "read100Pct": 25, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day78", "views": 4, "medianSeconds": 33, "read25Pct": 100, "read50Pct": 75, "read75Pct": 75, "read100Pct": 50, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day164", "views": 4, "medianSeconds": 4, "read25Pct": 75, "read50Pct": 75, "read75Pct": 75, "read100Pct": 75, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/manga-archive", "views": 4, "medianSeconds": 14, "read25Pct": 75, "read50Pct": 75, "read75Pct": 75, "read100Pct": 75, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/mecharm-not-done-yet", "views": 4, "medianSeconds": 5, "read25Pct": 100, "read50Pct": 75, "read75Pct": 75, "read100Pct": 75, "deadClicks": 0, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
{"path": "/record/day166", "views": 4, "medianSeconds": 3, "read25Pct": 100, "read50Pct": 100, "read75Pct": 100, "read100Pct": 100, "deadClicks": 6, "rageClicks": 0, "copies": 0, "videoPlays": 0, "outClicks": 0}
== referrers list 13
{"from": "（直接・不明）", "count": 1135}
{"from": "www.google.com", "count": 93}
{"from": "（サイト内）", "count": 46}
{"from": "search.yahoo.co.jp", "count": 6}
{"from": "www.linkedin.com", "count": 3}
{"from": "app.raindrop.io", "count": 2}
{"from": "gemini.google.com", "count": 1}
{"from": "pixelstomotors.github.io", "count": 1}
{"from": "www.city.oga.akita.jp", "count": 1}
{"from": "websearch.excite.co.jp", "count": 1}
{"from": "com.linkedin.android", "count": 1}
{"from": "www.youtube.com", "count": 1}
{"from": "t.co", "count": 1}
== areas list 10
{"area": "本文", "count": 42}
{"area": "ページの中身", "count": 22}
{"area": "ヘッダー", "count": 15}
{"area": "目次", "count": 8}
{"area": "sim", "count": 7}
{"area": "前後の記事", "count": 3}
{"area": "フッター", "count": 3}
{"area": "関連記事", "count": 2}
{"area": "その他", "count": 1}
{"area": "現在地", "count": 1}
== deadClicks list 25
{"where": "/record/day166 (リンクなし):p", "count": 6}
{"where": "/record/alphanist-binary-reward-assembly (リンクなし):p", "count": 5}
{"where": "/record/day126 (リンクなし):p", "count": 3}
{"where": "/record/decisions-api-grasp-check (リンクなし):img", "count": 3}
{"where": "/record/pi05-mecharm-first-real-run (リンクなし):p", "count": 3}
{"where": "/record/day126 (リンクなし):div", "count": 2}
{"where": "/manga/ep122 (リンクなし):img", "count": 2}
{"where": "/record/alphanist-binary-reward-assembly (リンクなし):td", "count": 2}
{"where": "/record/drag-teach-waypoint-replay (リンクなし):article", "count": 1}
{"where": "/record/day18 (リンクなし):section", "count": 1}
{"where": "/record/japan-robot-leader-physical-ai-gap (リンクなし):p", "count": 1}
{"where": "/ (リンクなし):div", "count": 1}
{"where": "/record/day126 (リンクなし):h2", "count": 1}
{"where": "/record/day126 (リンクなし):strong", "count": 1}
{"where": "/record/robot-car-kit-autonomy-breakdown (リンクなし):p", "count": 1}
{"where": "/record/pi05-mecharm-first-real-run (リンクなし):div", "count": 1}
{"where": "/record/openpi-remote-inference-first-run (リンクなし):p", "count": 1}
{"where": "/series6 (リンクなし):section", "count": 1}
{"where": "/issues (リンクなし):header", "count": 1}
{"where": "/series6 (リンクなし):div", "count": 1}
{"where": "/ (リンクなし):dl", "count": 1}
{"where": "/record/unity-family-restaurant-serving-robot (リンクなし):p", "count": 1}
{"where": "/record/unity-family-restaurant-serving-robot (リンクなし):img", "count": 1}
{"where": "/record/day18 (リンクなし):div", "count": 1}
{"where": "/record/day18 (リンクなし):code", "count": 1}
== moves list 34
{"move": "(戻る) → /", "count": 4}
{"move": "(戻る) → /series6", "count": 4}
{"move": "/privacypolicy → /", "count": 3}
{"move": "(戻る) → /issues", "count": 2}
{"move": "/record/building-a-physical-ai-lab-at-home → /record/openpi-remote-inference-first-run", "count": 2}
{"move": "(戻る) → /record/building-a-physical-ai-lab-at-home", "count": 2}
{"move": "(戻る) → /record/day166", "count": 2}
{"move": "/ → /record/drag-teach-waypoint-replay", "count": 1}
{"move": "/record/day18 → /record/day19", "count": 1}
{"move": "(戻る) → /record/day18", "count": 1}
{"move": "/record/pi05-mecharm-first-real-run → /issues", "count": 1}
{"move": "/issues → /manga-archive", "count": 1}
{"move": "/manga-archive → /manga/ep103", "count": 1}
{"move": "(戻る) → /manga-archive", "count": 1}
{"move": "/issues → /series6", "count": 1}
{"move": "/series6 → /record/teach-replay-19-demos", "count": 1}
{"move": "(戻る) → /record/pi05-mecharm-first-real-run", "count": 1}
{"move": "/record/pi05-mecharm-first-real-run → /series6", "count": 1}
{"move": "/series6 → /record/precision-is-not-learning-so101", "count": 1}
{"move": "/series6 → /record/building-a-physical-ai-lab-at-home", "count": 1}
{"move": "/series6 → /issues", "count": 1}
{"move": "/record/day166 → /series6", "count": 1}
{"move": "/record/day166 → /", "count": 1}
{"move": "/record/day166 → /issues", "count": 1}
{"move": "/record/day19 → /series1", "count": 1}
== clicks list 49
{"target": "/｜ヘッダー", "count": 4}
{"target": "btn:シリーズ｜ヘッダー", "count": 4}
{"target": "iframe:/embeds/day126-F4igYhgG.html｜本文", "count": 2}
{"target": "/issues｜ヘッダー", "count": 2}
{"target": "/series6｜ヘッダー", "count": 2}
{"target": "/record/openpi-remote-inference-first-run｜前後の記事", "count": 2}
{"target": "#index_R7T_TlUx｜目次", "count": 2}
{"target": "/series8｜本文", "count": 2}
{"target": "/record/drag-teach-waypoint-replay｜その他", "count": 1}
{"target": "#index_dLFWrz7T｜目次", "count": 1}
{"target": "/record/day19｜前後の記事", "count": 1}
{"target": "#index_JYtbwafr｜目次", "count": 1}
{"target": "/manga-archive｜ヘッダー", "count": 1}
{"target": "/manga/ep103｜ページの中身", "count": 1}
{"target": "/record/teach-replay-19-demos｜ページの中身", "count": 1}
{"target": "/record/precision-is-not-learning-so101｜ページの中身", "count": 1}
{"target": "/record/building-a-physical-ai-lab-at-home｜ページの中身", "count": 1}
{"target": "/series6｜フッター", "count": 1}
{"target": "/｜フッター", "count": 1}
{"target": "/issues｜フッター", "count": 1}
{"target": "#all｜ページの中身", "count": 1}
{"target": "/series1｜関連記事", "count": 1}
{"target": "iframe:/embeds/LOG357_aisle_width.html｜本文", "count": 1}
{"target": "#index_DrnosRK3｜目次", "count": 1}
{"target": "#index_06Y1F7DF｜目次", "count": 1}
```

### 出力：ページの種類ごとの数と、ハコブくんの記録

```python
import json
d=json.load(open('ev-stats.json'))
print(json.dumps(d['byWeek'][0],ensure_ascii=False))
bp=d['byPage']; s=sum(p['views'] for p in bp); print('pages',len(bp),'sum views',s)
from collections import Counter
print(Counter(p['views'] for p in bp).most_common(8))
for pre in ['/record/','/series','/manga','/issues','/sim','/serving','/gpu','/thinking','/one-year']:
    print(pre, sum(p['views'] for p in bp if p['path'].startswith(pre)))
for p in bp:
  if any(k in p['path'] for k in ['serving','series8','gpu','thinking','one-year','sim']): print(p['path'],p['views'],p['medianSeconds'])
h=json.load(open('hk-stats.json')); print(json.dumps(h,ensure_ascii=False)[:1500])
```

```
{"week": "2026-10-19", "label": "2026-10-19 〜 10-25", "views": 1270, "medianSeconds": 6, "read75Pct": 82, "spPct": 4, "backViews": 20, "outClicks": 0, "deadClicks": 69, "rageClicks": 1, "messageViews": 6, "formStarts": 0, "formSubmits": 0, "formSubmitsValid": 0, "messagesReceived": 0}
pages 100 sum views 542
[(3, 65), (4, 19), (6, 3), (11, 2), (9, 2), (5, 2), (126, 1), (20, 1)]
/record/ 276
/series 22
/manga 46
/issues 12
/sim 0
/serving 0
/gpu 0
/thinking 0
/one-year 0
/series8 8 16
/record/serving-robot-stage-b-18-runs 4 18
/record/serving-robot-observation-sheet 3 2
{"total": 1024, "byWeek": [{"week": "2026-10-19 〜 10-25", "shown": 907, "click": 0, "later": 13}, {"week": "2026-10-12 〜 10-18", "shown": 97, "click": 1, "later": 6}], "byDay": [{"day": "2026-10-25", "shown": 209, "click": 0, "later": 1}, {"day": "2026-10-24", "shown": 267, "click": 0, "later": 3}, {"day": "2026-10-23", "shown": 202, "click": 0, "later": 3}, {"day": "2026-10-22", "shown": 63, "click": 0, "later": 1}, {"day": "2026-10-21", "shown": 84, "click": 0, "later": 0}, {"day": "2026-10-20", "shown": 29, "click": 0, "later": 0}, {"day": "2026-10-19", "shown": 53, "click": 0, "later": 5}, {"day": "2026-10-18", "shown": 58, "click": 1, "later": 2}, {"day": "2026-10-17", "shown": 39, "click": 0, "later": 4}], "byItem": [{"id": "record:serving-robot-cost-subsidy", "shown": 132, "click": 0, "later": 0}, {"id": "record:cobot-deployment-law-and-coordination", "shown": 84, "click": 0, "later": 0}, {"id": "record:claude-project-linked-folders", "shown": 69, "click": 0, "later": 0}, {"id": "manga:ep121", "shown": 64, "click": 0, "later": 1}, {"id": "record:dev-mac-free-space", "shown": 59, "click": 0, "later": 1}, {"id": "record:audit-simulator-before-unity", "shown": 56, "click": 0, "later": 0}, {"id": "record:what-i-think-now-page", "shown": 56, "click": 0, "later": 0}, {"id": "record:serving-robot-video-part2", "shown": 56, "click": 0, "later": 0}, {"id": "record:serving-robot-stage-b-18-runs", "shown": 52, "click": 0, "later": 0}, {"id": "record:explainer-video-simulation-bac
```

上位100ページの表示の合計は542回で、残りの約730回は、表示が3回以下のページに散っていました。

### 出力：公開した見出しをビルドの結果で確かめた

```
dist/series8.html:<h1 class="sec-h s-title" data-astro-cid-kbb4royl>サービスロボット導入シミュレーション | Service Robot Simulation</h1>
dist/series6.html:<h1 class="sec-h s-title" data-astro-cid-kbb4royl>フィジカルAI研究室 | Physical AI Lab</h1>
<title>サービスロボット導入シミュレーション（Service Robot Simulation）| Pixels to Motors</title>
<h2 class="s-points-h" data-astro-cid-kbb4royl>シミュレーションで分かること</h2>
<h2 class="s-points-h" data-astro-cid-kbb4royl>試算の方法</h2>
<h2 class="sec-h" data-astro-cid-kbb4royl>試算を試す | Simulator</h2>
<h2 class="sec-h" data-astro-cid-kbb4royl>記録 | Record</h2>
```

```
<h1>Pixels to Motors | 人とロボットの関わりをシミュレーションでデザインする</h1>
<h2 class="sec-h">Pixels to Motors の1年 | Year One</h2>
<h2 class="sec-h">導入前ガイド | Guide</h2>
<h2 class="sec-h">4コママンガ | Archive</h2>
<h2 class="sec-h">いまの考え | Thinking</h2>
<h2 class="sec-h">シリーズ | Series</h2>
<h2 class="sec-h">記録 | Record</h2>
<h2 class="sec-h">事業所について | Sole Prop</h2>
<h1 class="msg-h">メッセージ | Message</h1>
<h1 class="pp-h">プライバシーポリシー | Privacy policy</h1>
<h1 class="sec-h ma-title">4コママンガ | Archive</h1>
<h1 class="sec-h ma-title">English 4-Koma | Archive</h1>
<h1 class="sec-h s-title">フィジカルAI研究室 | Physical AI Lab</h1>
<h2 class="sec-h">記録 | Record</h2>
<h2 class="sec-h">サービスロボット導入シミュレーションの記録 | Series</h2>
```

### 差分：全ページの見出しを日本語先にした（commit 44586e3）

```diff
commit 44586e3 全ページの見出しを日本語先にそろえた（トップ・記事・4コマ・メッセージ・プライバシーポリシー）

diff --git a/src/components/MangaPage.astro b/src/components/MangaPage.astro
index 115bc64..07237d6 100644
--- a/src/components/MangaPage.astro
+++ b/src/components/MangaPage.astro
@@ -36,7 +36,7 @@ const jsonLd = {
   </article>
   <section class="related">
     <div class="panel">
-      <h2 class="sec-h">Related Records | マンガの背景にある記録</h2>
+      <h2 class="sec-h">マンガの背景にある記録 | Related Records</h2>
       <LoadMore limit={6} total={related.length}>{related.map((c) => <RecordCard item={c} />)}</LoadMore>
     </div>
   </section>
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 99632e1..7f3a77a 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -62,18 +62,18 @@ const jsonLd = [
   <!-- 1年の LP（/one-year）への入口。動画の次、導入前ガイドの前に置く（2026-10-25）
        MASTER 2026-10-25：デジタルの AI の土台を記録から示すページは、トップと SNS に出してよい（AI活用の相談の看板にはしない） -->
   <section class="sec"><div class="panel">
-    <h2 class="sec-h">Year One | Pixels to Motors の1年</h2>
+    <h2 class="sec-h">Pixels to Motors の1年 | Year One</h2>
     <OneYearBanner />
   </div></section>
 
   <!-- 導入前ガイド：LP 2つ（GPU を買うか借りるか・配膳ロボットの導入前シミュレーション）への入口 -->
   <section class="sec"><div class="panel">
-    <h2 class="sec-h">Guide | 導入前ガイド</h2>
+    <h2 class="sec-h">導入前ガイド | Guide</h2>
     <GuideCards />
   </div></section>
 
   <section class="sec"><div class="panel">
-    <h2 class="sec-h">Archive | 4コママンガ</h2>
+    <h2 class="sec-h">4コママンガ | Archive</h2>
     <div class="grid4">{mangaCards.map((m) => <MangaCard item={m} />)}</div>
     <div class="center"><a class="btn-more" href="/manga-archive">もっと見る<i class="ms">arrow_forward</i></a></div>
   </div></section>
@@ -81,12 +81,12 @@ const jsonLd = [
   <!-- 考えのページ（/thinking）への入口。4コマとシリーズの間に置く
        （2026-10-24 のアクセスの記録：TOP は25%の深さまでで42%、下まで33%しか読まれない。一番下では届かないため上げた。最初の画面の直下は上すぎるとエイイチの判断） -->
   <section class="sec"><div class="panel">
-    <h2 class="sec-h">Thinking | いまの考え</h2>
+    <h2 class="sec-h">いまの考え | Thinking</h2>
     <ThinkingBanner />
   </div></section>
 
   <section class="sec"><div class="panel">
-    <h2 class="sec-h">Series | シリーズ</h2>
+    <h2 class="sec-h">シリーズ | Series</h2>
     <div class="grid2">
       {series.map((s) => (
         <a class="scard" href={s.href}>
@@ -101,13 +101,13 @@ const jsonLd = [
   </div></section>
 
   <section class="sec" id="record"><div class="panel">
-    <h2 class="sec-h">Record | 記録</h2>
+    <h2 class="sec-h">記録 | Record</h2>
     <LoadMore limit={12} total={records.length}>{records.map((c) => <RecordCard item={c} />)}</LoadMore>
     <div class="embed-banner"><iframe src="/embeds/top-banner.html" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation" title="お知らせ" data-embed style="height:240px"></iframe></div>
   </div></section>
 
   <section class="sec" id="about"><div class="panel">
-    <h2 class="sec-h">Sole Prop | 事業所について</h2>
+    <h2 class="sec-h">事業所について | Sole Prop</h2>
     <div class="prop">
       <div class="prop-person">
         <video class="prop-photo" src={media("/media/s-1280x720_ff64009f-cd40-4e7d-8ba3-350ff0d6c3ad.mp4")} poster={media("/media/s-2048x2048_5af0bc9f-8d23-4bdb-85e4-ada51bfd7bbd.webp")} muted autoplay loop playsinline></video>
diff --git a/src/pages/manga-archive-en.astro b/src/pages/manga-archive-en.astro
index 29a39e4..941a5ec 100644
--- a/src/pages/manga-archive-en.astro
+++ b/src/pages/manga-archive-en.astro
@@ -2,5 +2,5 @@
 import MangaArchive from '../components/MangaArchive.astro';
 import { allEnglish } from '../lib/records';
 ---
-<MangaArchive items={allEnglish} heading="Archive | English 4-Koma" other={{ href: '/manga-archive', label: '4コママンガ' }}
+<MangaArchive items={allEnglish} heading="English 4-Koma | Archive" other={{ href: '/manga-archive', label: '4コママンガ' }}
   base="/english-4-koma" path="/manga-archive-en" />
diff --git a/src/pages/manga-archive.astro b/src/pages/manga-archive.astro
index 42aafca..42c4ca5 100644
--- a/src/pages/manga-archive.astro
+++ b/src/pages/manga-archive.astro
@@ -2,5 +2,5 @@
 import MangaArchive from '../components/MangaArchive.astro';
 import { allManga } from '../lib/records';
 ---
-<MangaArchive items={allManga} heading="Archive | 4コママンガ" other={{ href: '/manga-archive-en', label: 'English 4-Koma' }}
+<MangaArchive items={allManga} heading="4コママンガ | Archive" other={{ href: '/manga-archive-en', label: 'English 4-Koma' }}
   base="/manga" path="/manga-archive" />
diff --git a/src/pages/message.astro b/src/pages/message.astro
index 657e959..44be7ac 100644
--- a/src/pages/message.astro
+++ b/src/pages/message.astro
@@ -4,7 +4,7 @@ import Base from '../layouts/Base.astro';
 <Base title="お問い合わせ（メッセージ） | Pixels to Motors" description="Pixels to Motors へのお問い合わせ。ロボットやフィジカルAIの体験設計、HRI デザインなど、仕事のご依頼やご相談をお聞かせください。1〜2営業日以内にお返事します。" path="/message">
   <section class="msg">
     <div class="msg-lead">
-      <h1 class="msg-h">Message | メッセージ</h1>
+      <h1 class="msg-h">メッセージ | Message</h1>
       <p>Pixels to Motorsにご興味を持っていただきありがとうございます。仕事のご依頼や、ご相談内容をお聞かせください。1〜2営業日以内にお返事いたします。</p>
     </div>
     <form class="msg-form" id="msgForm" novalidate>
diff --git a/src/pages/privacypolicy.astro b/src/pages/privacypolicy.astro
index a9c2919..8550873 100644
--- a/src/pages/privacypolicy.astro
+++ b/src/pages/privacypolicy.astro
@@ -18,7 +18,7 @@ const revised = '2026年10月23日';
 ---
 <Base title="プライバシーポリシー | Pixels to Motors" description="Pixels to Motors（ADAPT DESIGN）のプライバシーポリシー。お預かりする個人情報と技術データの扱い、メッセージフォームの情報の保存、Cookie を使わないアクセス解析とサイトの使われ方の記録、利用している外部サービスについて説明しています。" path="/privacypolicy">
   <article class="pp">
-    <h1 class="pp-h">Privacy policy | プライバシーポリシー</h1>
+    <h1 class="pp-h">プライバシーポリシー | Privacy policy</h1>
     <p class="pp-p">Pixels to Motors（運営：ADAPT DESIGN　島津 栄一。以下「当方」といいます。）は、当方の提供するサービス（以下「本サービス」といいます。）における、ユーザーについての個人情報を含む利用者情報の取扱いについて、以下のとおりプライバシーポリシー（以下「本ポリシー」といいます。）を定めます。</p>
     {sections.map(([h, b]) => (<><h2 class="pp-h2">{h}</h2><p class="pp-p" set:html={nl(b)} /></>))}
     <p class="pp-p pp-date">最終改定日：{revised}</p>
diff --git a/src/pages/record/[slug].astro b/src/pages/record/[slug].astro
index fd63538..b580571 100644
--- a/src/pages/record/[slug].astro
+++ b/src/pages/record/[slug].astro
@@ -146,7 +146,7 @@ const jsonLd = [
   {nearby.length > 0 && (
     <section class="related">
       <div class="panel">
-        <h2 class="sec-h">Series | {seriesName}の記録</h2>
+        <h2 class="sec-h">{seriesName}の記録 | Series</h2>
         <div class="grid3">{nearby.map((c) => <RecordCard item={c} />)}</div>
         {seriesPage && <div class="rel-more"><a class="btn-dark" href={`/${seriesPage[0]}`}>{seriesName}の記録をすべて見る（{sameSeries.length}本）<i class="ms">arrow_forward</i></a></div>}
       </div>
```

### 差分：トップの解説動画の見出し public/embeds/top-video.html（commit c80d7b1）

```diff
commit c80d7b1 トップの解説動画の見出しも日本語先に

diff --git a/public/embeds/top-video.html b/public/embeds/top-video.html
index 7580782..ba9d8df 100644
--- a/public/embeds/top-video.html
+++ b/public/embeds/top-video.html
@@ -129,7 +129,7 @@
 
 
 <section class="ptm-video" aria-roledescription="carousel" aria-label="解説動画">
-  <h2>Video | 解説動画</h2>
+  <h2>解説動画 | Video</h2>
 
   <div class="ptm-viewport">
     <div class="ptm-track" id="ptm-track" style="transform: translateX(calc(-100% - 32px));"><div class="ptm-slide" role="group" aria-roledescription="slide" aria-label="1 / 6"><a class="ptm-card" href="https://www.youtube.com/watch?v=drHOxzvrcn8" target="_blank" rel="noopener" draggable="false" tabindex="-1"><div class="ptm-thumb"><img src="https://i.ytimg.com/vi/drHOxzvrcn8/maxresdefault.jpg" data-fallback="https://i.ytimg.com/vi/drHOxzvrcn8/hqdefault.jpg" alt="" width="1280" height="720" draggable="false"><span class="ptm-dur">13:36</span></div><div class="ptm-body"><div class="ptm-meta"><p class="ptm-new">NEW</p><p class="ptm-tag">フィジカルAI解説</p><p class="ptm-date">2026/10/15</p></div><p class="ptm-title">AIエージェントにロボットのコードを書かせたら、原因を7回取り違えた</p><p class="ptm-desc">エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。</p></div></a></div><div class="ptm-slide" role="group" aria-roledescription="slide" aria-label="2 / 6"><a class="ptm-card" href="https://www.youtube.com/watch?v=48XTmtYkNRg" target="_blank" rel="noopener" draggable="false" tabindex="0"><div class="ptm-thumb"><img src="https://i.ytimg.com/vi/48XTmtYkNRg/maxresdefault.jpg" data-fallback="https://i.ytimg.com/vi/48XTmtYkNRg/hqdefault.jpg" alt="" width="1280" height="720" loading="lazy" draggable="false"><span class="ptm-dur">10:28</span></div><div class="ptm-body"><div class="ptm-meta"><p class="ptm-new">NEW</p><p class="ptm-tag">フィジカルAI解説</p><p class="ptm-date">2026/10/14</p></div><p class="ptm-title">ジャングリア沖縄の赤字89億円、AIとロボットで埋められるか勝手に試算してみた</p><p class="ptm-desc">エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。</p></div></a></div><div class="ptm-slide" role="group" aria-roledescription="slide" aria-label="3 / 6"><a class="ptm-card" href="https://www.youtube.com/watch?v=wFg77gRg8RU" target="_blank" rel="noopener" draggable="false" tabindex="-1"><div class="ptm-thumb"><img src="https://i.ytimg.com/vi/wFg77gRg8RU/maxresdefault.jpg" data-fallback="https://i.ytimg.com/vi/wFg77gRg8RU/hqdefault.jpg" alt="" width="1280" height="720" loading="lazy" draggable="false"><span class="ptm-dur">14:17</span></div><div class="ptm-body"><div class="ptm-meta"><p class="ptm-new">NEW</p><p class="ptm-tag">フィジカルAI解説</p><p class="ptm-date">2026/10/14</p></div><p class="ptm-title">フィジカルAIのお金はどこへ流れているか｜日本・米国・中国のスタートアップ投資</p><p class="ptm-desc">エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。</p></div></a></div><div class="ptm-slide" role="group" aria-roledescription="slide" aria-label="4 / 6"><a class="ptm-card" href="https://www.youtube.com/watch?v=3lwqALyXiY8" target="_blank" rel="noopener" draggable="false" tabindex="-1"><div class="ptm-thumb"><img src="https://i.ytimg.com/vi/3lwqALyXiY8/maxresdefault.jpg" data-fallback="https://i.ytimg.com/vi/3lwqALyXiY8/hqdefault.jpg" alt="" width="1280" height="720" loading="lazy" draggable="false"><span class="ptm-dur">14:43</span></div><div class="ptm-body"><div class="ptm-meta"><p class="ptm-new">NEW</p><p class="ptm-tag">フィジカルAI解説</p><p class="ptm-date">2026/10/13</p></div><p class="ptm-title">自動運転は「暴走」するのか ― ロボタクシーのリコールの届け出に、ハッキングの記載はなかった</p><p class="ptm-desc">エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。</p></div></a></div><div class="ptm-slide" role="group" aria-roledescription="slide" aria-label="5 / 6"><a class="ptm-card" href="https://www.youtube.com/watch?v=wGlD_LJrlPo" target="_blank" rel="noopener" draggable="false" tabindex="-1"><div class="ptm-thumb"><img src="https://i.ytimg.com/vi/wGlD_LJrlPo/maxresdefault.jpg" data-fallback="https://i.ytimg.com/vi/wGlD_LJrlPo/hqdefault.jpg" alt="" width="1280" height="720" loading="lazy" draggable="false"><span class="ptm-dur">12:30</span></div><div class="ptm-body"><div class="ptm-meta"><p class="ptm-new">NEW</p><p class="ptm-tag">フィジカルAIの現在地</p><p class="ptm-date">2026/10/12</p></div><p class="ptm-title">人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか</p><p class="ptm-desc">エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。</p></div></a></div><div class="ptm-slide" role="group" aria-roledescription="slide" aria-label="6 / 6"><a class="ptm-card" href="https://www.youtube.com/watch?v=iaJLQYEg_vE" target="_blank" rel="noopener" draggable="false" tabindex="-1"><div class="ptm-thumb"><img src="https://i.ytimg.com/vi/iaJLQYEg_vE/maxresdefault.jpg" data-fallback="https://i.ytimg.com/vi/iaJLQYEg_vE/hqdefault.jpg" alt="" width="1280" height="720" loading="lazy" draggable="false"><span class="ptm-dur">2:54</span></div><div class="ptm-body"><div class="ptm-meta"><p class="ptm-tag">フィジカルAI解説</p><p class="ptm-date">2026/03/24</p></div><p class="ptm-title">R2R2Rプロジェクト - mechArm 270Pi 開封の儀</p><p class="ptm-desc">エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。</p></div></a></div></div>
```

### 差分：シリーズのページの見出しと series8 の説明文（commit 62b9110）

```diff
commit 62b9110 シリーズのページの見出しを日本語先に、series8 に定義の一文と一覧を追加

diff --git a/src/lib/records.ts b/src/lib/records.ts
index 288f61d..f3d0fbd 100644
--- a/src/lib/records.ts
+++ b/src/lib/records.ts
@@ -34,7 +34,7 @@ export type SimTab = { id: string; task: string; place: string; src: string; tit
 
 // シリーズのページ（URL → 記事のシリーズ名）。Studio の一覧の絞り込み条件と同じ
 // intro はシリーズのページの見出しの下に出す説明（ないシリーズは出さない）
-export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number; intro?: string; guide?: boolean; thinking?: boolean; serving?: boolean; sim?: { src: string; title: string; note: string; obs?: { src: string; title: string; label: string; note: string; points: string[] }; tabs?: SimTab[] } }> = {
+export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number; intro?: string; points?: { h: string; items: string[] }[]; guide?: boolean; thinking?: boolean; serving?: boolean; sim?: { src: string; title: string; note: string; obs?: { src: string; title: string; label: string; note: string; points: string[] }; tabs?: SimTab[] } }> = {
   series1: { series: 'HRI', title: 'Drug Delivery Robot | 薬剤搬送ロボットハコブくん', pageTitle: 'Series1 | 薬剤搬送ロボット', limit: 12,
     intro: '病院で薬を運ぶロボット「ハコブくん」を題材に、人とロボットの関わり（HRI）を設計している記録です。病院で使う画面（UI）の設計、Unity での自律移動、Fusion での形づくり、分身ロボット（アバター）の考察までを、4コママンガと一緒に残しています。' },
   series2: { series: 'R2R2R', title: 'R2R2R | デジタルツイン学習-R2R2R（Real to Render to Real）', pageTitle: 'Series2 | R2R2R', limit: 12,
@@ -50,7 +50,12 @@ export const SERIES_PAGES: Record<string, { series: string; title: string; pageT
   series7: { series: '画像認識・物体検出', title: 'Image recognition and Object detection | 画像認識と物体検出', pageTitle: 'Series7 | 画像認識と物体検出', limit: 12,
     intro: 'ロボットのカメラが物を見つける仕組みを、下の層から学び直している記録です。画素の数字、色の見分け方、しきい値、輪郭、特徴点といった昔からの手法を、机の上のアームを撮った写真で1つずつ測って確かめ、その上で SAM 3 のような新しいモデルも試します。評価の物差し（IoU・適合率・再現率など）を先に決め、結果は数字で残しています。' },
   series8: { series: '導入シミュレーション', title: 'Service Robot Simulation | サービスロボット導入シミュレーション', pageTitle: 'Series8 | サービスロボット導入シミュレーション', limit: 12, serving: true,
-    intro: '配膳・清掃・搬送など、店や施設で動くサービスロボットを入れたら何が変わるかを、導入前に試算する記録です。時間・順番・数は SimPy で、ぶつかる・こぼれるといった空間と動きは Unity で扱います。前提の数字は公開事例と観察から置き、結果とコードは全量で公開しています。',
+    intro: 'サービスロボット導入シミュレーションとは、配膳・清掃・搬送などのロボットを店や施設に入れる前に、待ち時間・必要な人数・ロボットが止まる場所・費用を試算して確かめることです。このシリーズでは、時間・順番・台数を SimPy で、ぶつかる・こぼれるといった空間と動きを Unity で扱い、結果とコードを全量公開しています。',
+    // 見出しの下に並べる短い一覧（検索や AI が「何が分かるか・どう試算するか」を拾えるように、本文の文字で書く）
+    points: [
+      { h: 'シミュレーションで分かること', items: ['必要な人数：ロボットを何台入れれば、スタッフを何人減らせるか', '待ち時間：ピーク時の入店待ちと、料理が届くまでの時間', '止まる場所：通路や席の配置で、ロボットが詰まる箇所', '費用：ロボットの月額と、浮く人件費の比較（補助金を含む）'] },
+      { h: '試算の方法', items: ['時間・順番・台数：離散イベントシミュレーション（SimPy）', '空間と動き：3Dシミュレーション（Unity）', '前提の数字：公開事例と、店で数える観察シート', 'ブラウザで試せる試算：飲食店・病院・商業施設・ホテル・オフィス・屋外の6現場'] },
+    ],
     sim: { src: '/sim/serving-robot.html', title: '配膳ロボット、何台入れたら店は変わる？',
       note: '店を選び、人数・台数・ロボットの使い方を切り替えると、ピーク時の入店待ちや料理が届くまでの時間が変わります。標準ファミレスは、店の形（平面図）から選ぶこともできます。レジの数（1つ・2つ）も選べます。ロボットの費用と浮く人件費も比べられます（補助金の情報つき）。1つの条件を40回回した平均で、数字はすべて仮の前提による試算です（試作 v2.8・32,850条件）。',
       obs: { src: '/sim/serving-robot-observation.html', title: '試算の仮の数字を、店で数える観察シート', label: '観察シートを開く ↗',
diff --git a/src/pages/[series].astro b/src/pages/[series].astro
index 8bee9a0..9e410ba 100644
--- a/src/pages/[series].astro
+++ b/src/pages/[series].astro
@@ -19,17 +19,26 @@ const conf = SERIES_PAGES[series];
 const blocks = (staticPages[series]?.blocks || []).slice(1); // 先頭はページの見出し
 const items = await seriesCards(conf.series);
 const [enName, jaName] = conf.title.split(' | ');
+// 見出しは日本語を先に置く（検索結果のタイトルが見出しに置き換えられるため。2026-10-25）
+const heading = jaName ? `${jaName} | ${enName}` : conf.title;
 const description = conf.intro ? `${SITE_NAME} のシリーズ「${jaName || conf.title}」（${enName}）。${conf.intro}` : `${SITE_NAME} のシリーズ「${jaName || conf.title}」（${enName}）の記録一覧。ロボットとフィジカルAIを、29年のデザイナー経験から実機とシミュレーションで試した過程を、失敗も含めて公開しています。`;
 const jsonLd = {
-  '@context': 'https://schema.org', '@type': 'CollectionPage', name: conf.title, description, url: `${SITE}/${series}`,
+  '@context': 'https://schema.org', '@type': 'CollectionPage', name: heading, description, url: `${SITE}/${series}`,
   inLanguage: 'ja',
   hasPart: items.slice(0, 50).map((c) => ({ '@type': 'BlogPosting', headline: c.title, url: `${SITE}/record/${c.slug}` })),
 };
 ---
 <Base title={`${jaName || conf.title}（${enName}）| ${SITE_NAME}`} description={description} path={`/${series}`} jsonLd={jsonLd}>
   <section class="s-hero"><div class="panel">
-    <h1 class="sec-h s-title">{conf.title}</h1>
+    <h1 class="sec-h s-title">{heading}</h1>
     {conf.intro && <p class="s-intro">{conf.intro}</p>}
+    {conf.points && (
+      <div class="s-points">
+        {conf.points.map((p) => (
+          <div><h2 class="s-points-h">{p.h}</h2><ul class="s-obs-list">{p.items.map((t) => <li>{t}</li>)}</ul></div>
+        ))}
+      </div>
+    )}
     {conf.guide && <div class="s-guide"><GuideBanner /></div>}
     {conf.thinking && <div class="s-guide"><ThinkingBanner /></div>}
     {conf.serving && <div class="s-guide"><ServingBanner /></div>}
@@ -43,7 +52,7 @@ const jsonLd = {
   </div></section>
   {conf.sim && (
     <section class="sec s-sim" data-ev-area="sim"><div class="panel">
-      <h2 class="sec-h">Simulator | 試算を試す</h2>
+      <h2 class="sec-h">試算を試す | Simulator</h2>
       {conf.sim.tabs && (
         <div class="s-tabs" role="tablist" aria-label="業種とロボットの仕事">
           {conf.sim.tabs.map((t, i) => (
@@ -134,7 +143,7 @@ const jsonLd = {
     }
   </script>
   <section class="sec"><div class="panel">
-    <h2 class="sec-h">Record | 記録</h2>
+    <h2 class="sec-h">記録 | Record</h2>
     <LoadMore limit={conf.limit} total={items.length}>{items.map((c) => <RecordCard item={c} />)}</LoadMore>
   </div></section>
 </Base>
@@ -143,6 +152,10 @@ const jsonLd = {
   .s-title { margin: 0; }
   .s-guide { margin: 24px 0 0; }
   .s-intro { margin: 16px 0 0; font: 400 16px/28px var(--sans); color: var(--ink-2); }
+  .s-points { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; margin: 24px 0 0; }
+  .s-points-h { margin: 0 0 8px; font: 600 16px/26px var(--sans); color: var(--ink); }
+  .s-points .s-obs-list { margin: 0; }
+  @media (max-width: 767px) { .s-points { grid-template-columns: minmax(0, 1fr); gap: 20px; } }
   .s-label { display: block; margin: 40px 0 16px; font: 500 16px/24px var(--sans); color: var(--ink); }
   .s-title + .s-label { margin-top: 8px; }
   a.s-label:hover { text-decoration: underline; }
```

### 差分：人と機械の印（commit 7b50e3e）

```diff
commit 7b50e3e サイトの使われ方の記録に、人と機械を分ける印を足した

diff --git a/src/components/SiteMeasure.astro b/src/components/SiteMeasure.astro
index b05fd71..8f07cef 100644
--- a/src/components/SiteMeasure.astro
+++ b/src/components/SiteMeasure.astro
@@ -9,7 +9,9 @@
 // 送るもの：ページ・サイト内のどのページから来たか（戻るボタンで戻ったか）・外から来たならそのサイトの名前（ドメイン）だけ・
 // 画面の幅（pc/sp）・どこまで読んだか・表示していた時間・押したリンクやボタン（行き先とページのどの部分か）・
 // リンクでない所を押した回数・短い間に同じ所を何度も押した回数・コピーした回数・動画を再生した回数・
-// フォームに入力を始めたか・送信ボタンを押した回数
+// フォームに入力を始めたか・送信ボタンを押した回数・
+// 人が操作したか（マウスを動かす・押す・指で触れる・ホイール・キーのどれかがあったか）・
+// 自動で動くブラウザか（navigator.webdriver と、ブラウザの名乗りに Headless・bot などが入っているかの判定結果だけ）
 // 送らないもの：入力した内容・コピーした文章・IP アドレス・ブラウザの種類・Cookie。
 // 番号はこの1回の表示のためだけに作り、ブラウザに保存しない（別のページや次の来訪とはつながらない）。
 //
@@ -46,6 +48,7 @@
   }
 
   if (!off) {
+    const AUTO_UA = /headless|lighthouse|bot\/|bot;|\bbot\b|crawl|spider|slurp|preview|phantom|puppeteer|playwright|selenium/i;
     const newId = () => {
       try { return crypto.randomUUID().replace(/-/g, '').slice(0, 12); } catch { return Math.random().toString(36).slice(2, 14); }
     };
@@ -64,9 +67,14 @@
         v: newId(), n: 0, p: location.pathname.slice(0, 160), f: from ?? o.f, r: from ? '' : o.r,
         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＝自動で動くブラウザの印（名乗りそのものは送らない）
+        u: 0, wd: navigator.webdriver ? 1 : 0, hl: AUTO_UA.test(navigator.userAgent || '') ? 1 : 0,
       };
     };
     let st = fresh();
+    // 人の操作。1回あれば十分なので、最初の1回だけ印を付ける
+    const touched = () => { if (!st.u) { st.u = 1; dirty = true; } };
+    for (const ev of ['pointermove', 'pointerdown', 'touchstart', 'wheel', 'keydown']) document.addEventListener(ev, touched, { capture: true, passive: true });
     let visibleSince = document.hidden ? 0 : Date.now();
     let dirty = true;
 
diff --git a/src/pages/privacypolicy.astro b/src/pages/privacypolicy.astro
index 8550873..5fd396a 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フォームに入力された内容やコピーした文章そのものは記録しません。IP アドレス・ブラウザの種類・Cookie も記録しません。記録をまとめるために1回の表示ごとに一時的な番号を作りますが、この番号はブラウザに保存されず、別のページや次回の来訪とは結び付きません。記録の保存には Cloudflare, Inc. のサービス（Cloudflare R2）を利用しています。\nブラウザで「トラッキングしない」（Do Not Track）または Global Privacy Control を設定している場合は、この記録を行いません。'],
+  ['サイトの使われ方の記録について', '当サイトでは、読みやすさや使いやすさを改善するため、ページを1回表示するごとに、次の内容を記録します。\n・表示したページと、サイト内のどのページから来たか（戻るボタンで戻ったかどうか）。外のサイトから来た場合は、そのサイトの名前（ドメイン）だけ\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 2230ef8..89060c7 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -276,6 +276,16 @@ h1{font-size:20px}h2{font-size:16px;margin-top:28px}table{border-collapse:collap
 const EV_AREAS = new Set(['hakobu', 'header', 'footer', 'crumbs', 'toc', 'consult', 'pager', 'related', 'body', 'main', 'sim', 'other']);
 const EV_DEPTHS = new Set([0, 25, 50, 75, 100]);
 const evInt = (x, max) => { const n = Math.floor(Number(x)); return Number.isFinite(n) && n >= 0 ? Math.min(n, max) : 0; };
+// 自動で巡回するプログラムの名乗り（検索エンジン・AI のクローラー・SNS のプレビュー・自動のブラウザ）。名乗りそのものは保存しない
+const EV_BOT_UA = /bot\/|bot;|\bbot\b|crawl|spider|slurp|headless|lighthouse|preview|facebookexternalhit|embedly|python-|curl\/|wget|go-http|okhttp|axios|node-fetch|playwright|puppeteer|selenium|phantom/i;
+// 人か機械か：b＝機械（自動のブラウザ・クローラーの名乗り）／h＝人の操作があった／x＝どちらとも言えない（操作なし）。2026-10-25 から
+const evKind = (request, d) => {
+  const ua = request.headers.get('user-agent') || '';
+  let verified = false;
+  try { verified = !!(request.cf && (request.cf.verifiedBotCategory || (request.cf.botManagement && request.cf.botManagement.verifiedBot))); } catch { /* 使えない環境では名乗りだけで見る */ }
+  if (verified || !ua || EV_BOT_UA.test(ua) || Number(d.wd) === 1 || Number(d.hl) === 1) return 'b';
+  return Number(d.u) === 1 ? 'h' : 'x';
+};
 const evText = (x, max) => String(x == null ? '' : x).replace(/[\u0000-\u001f|>]/g, ' ').slice(0, max);
 
 async function evRecord(request, env, url) {
@@ -302,6 +312,7 @@ async function evRecord(request, env, url) {
     c,
     d: String(evInt(d.d, 999)), g: String(evInt(d.g, 999)), cp: String(evInt(d.cp, 999)), vp: String(evInt(d.vp, 999)),
     fs: String(evInt(d.fs, 1)), fx: String(evInt(d.fx, 99)), fv: String(evInt(d.fv, 99)),
+    k: evKind(request, d),
   };
   const at = new Date();
   const day = new Date(at.getTime() + 9 * 3600e3).toISOString().slice(0, 10); // 日本時間の日付
@@ -318,6 +329,9 @@ async function evStats(request, env, url) {
   if (request.method !== 'GET') return new Response('Method Not Allowed', { status: 405 });
   const since = /^\d{4}-\d{2}-\d{2}$/.test(url.searchParams.get('since') || '') ? url.searchParams.get('since') : '';
   const week = /^\d{4}-\d{2}-\d{2}$/.test(url.searchParams.get('week') || '') ? url.searchParams.get('week') : '';
+  // who=human で人だけ、who=bot で機械だけ、who=unknown で操作のなかった表示だけ、who=old で印を付ける前の記録だけを数える（なければ全部）
+  const WHO = { human: 'h', bot: 'b', unknown: 'x', old: 'o' };
+  const who = WHO[url.searchParams.get('who')] ? url.searchParams.get('who') : '';
   // 1) 記録を読む（v ごとに n が最大の1件）
   const views = new Map();
   let cursor, pages = 0, truncated = false;
@@ -354,12 +368,17 @@ async function evStats(request, env, url) {
     ...m, week: evWeekOf(m.day), s: Number(m.s) || 0, t: Number(m.t) || 0,
     d: Number(m.d) || 0, g: Number(m.g) || 0, cp: Number(m.cp) || 0, vp: Number(m.vp) || 0,
     fs: Number(m.fs) || 0, fx: Number(m.fx) || 0, fv: Number(m.fv) || 0,
+    k: m.k || 'o',
     clicks: m.c ? m.c.split('|').map((x) => { const i = x.lastIndexOf('>'); return { target: x.slice(0, i), area: x.slice(i + 1) }; }) : [],
   }));
+  // 人と機械の内訳は、絞り込む前の全部の記録で週ごとに数える
+  const kinds = new Map();
+  for (const r of rows) { const x = kinds.get(r.week) || { h: 0, b: 0, x: 0, o: 0 }; x[r.k]++; kinds.set(r.week, x); }
+  const counted = who ? rows.filter((r) => r.k === WHO[who]) : rows;
   const isMsg = (p) => p === '/message' || p === '/message/';
   const outside = (c) => c.area !== 'hakobu' && !c.target.startsWith('/') && !c.target.startsWith('#') && !c.target.startsWith('btn:') && !c.target.startsWith('(リンクなし)') && !c.target.startsWith('iframe');
   const weeks = new Map();
-  for (const r of rows) {
+  for (const r of counted) {
     const w = weeks.get(r.week) || { week: r.week, views: 0, times: [], read75: 0, dead: 0, rage: 0, out: 0, back: 0, sp: 0, msgViews: 0, formStart: 0, formSubmit: 0, formValid: 0 };
     w.views++; w.times.push(r.t); if (r.s >= 75) w.read75++; w.dead += r.d; w.rage += r.g;
     w.out += r.clicks.filter(outside).length; if (r.f === '(戻る)') w.back++; if (r.w === 'sp') w.sp++;
@@ -374,10 +393,11 @@ async function evStats(request, env, url) {
       week: w.week, label: `${w.week} 〜 ${end.toISOString().slice(5, 10)}`, views: w.views, medianSeconds: evMedian(w.times),
       read75Pct: pct(w.read75, w.views), spPct: pct(w.sp, w.views), backViews: w.back, outClicks: w.out, deadClicks: w.dead, rageClicks: w.rage,
       messageViews: w.msgViews, formStarts: w.formStart, formSubmits: w.formSubmit, formSubmitsValid: w.formValid, messagesReceived: sent.get(w.week) || 0,
+      humanViews: (kinds.get(w.week) || {}).h || 0, botViews: (kinds.get(w.week) || {}).b || 0, unknownViews: (kinds.get(w.week) || {}).x || 0, unmarkedViews: (kinds.get(w.week) || {}).o || 0,
     };
   });
   // ページごと・移り方・押したもの（week= を付けるとその週だけ。付けなければ全期間）
-  const scope = week ? rows.filter((r) => r.week === week) : rows;
+  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();
   for (const r of scope) {
@@ -396,7 +416,7 @@ async function evStats(request, env, url) {
   }
   const top = (map, n, key) => [...map].sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => ({ [key]: k, count: v }));
   const data = {
-    total: rows.length, truncated, since: since || null, week: week || null,
+    total: rows.length, counted: counted.length, truncated, since: since || null, week: week || null, who: who || null,
     byWeek,
     byPage: [...pagesMap.values()].sort((a, b) => b.views - a.views).slice(0, 100).map((p) => ({
       path: p.path, views: p.views, medianSeconds: evMedian(p.times),
@@ -420,7 +440,8 @@ async function evStats(request, env, url) {
 <title>サイトの使われ方の記録</title><style>body{font:14px/1.7 -apple-system,'Noto Sans JP',sans-serif;margin:24px;color:#242424;background:#f7f7f7}
 h1{font-size:20px}h2{font-size:16px;margin-top:28px}.wrap{overflow-x:auto}table{border-collapse:collapse;background:#fff}th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;white-space:nowrap}th{background:#eee}p{color:#555}</style></head><body>
 <h1>サイトの使われ方の記録</h1><p>1回のページ表示ごとに、どこまで読んだか・何を押したか・迷ったかを数えたもの。誰かは分からない。日付は日本時間、週は月曜はじまり。${week ? `ページごと以下の表は ${esc(week)} の週だけ。` : 'ページごと以下の表は全期間。?week=週の月曜の日付 でその週だけになる。'}${truncated ? ' 記録が多すぎて一部だけ数えた（?since=日付 で期間を絞る）。' : ''}</p>
-<h2>週ごと</h2>${table([['label', '週'], ['views', '表示'], ['medianSeconds', '表示時間（中央値・秒）'], ['read75Pct', '75％以上読んだ（％）'], ['spPct', 'スマホ（％）'], ['backViews', '戻るで戻った'], ['outClicks', '外へのクリック'], ['deadClicks', 'リンクなしを押した'], ['rageClicks', '連打'], ['messageViews', 'フォームの表示'], ['formStarts', '入力を始めた'], ['formSubmits', '送信を押した'], ['formSubmitsValid', 'うち入力がそろっていた'], ['messagesReceived', '届いたメッセージ']], byWeek)}
+<p>人と機械：「人」はマウス・指・キーの操作があった表示、「機械」はクローラーや自動のブラウザの名乗り・印があった表示、「操作なし」はどちらとも言えない表示、「印なし」は 2026-10-25 より前の記録。いま数えているのは <b>${esc({ human: '人', bot: '機械', unknown: '操作なし', old: '印なし' }[who] || '全部')}</b>（?who=human・bot・unknown・old で絞る。週ごとの表の「人」〜「印なし」の4列は常に全部の内訳）。</p>
+<h2>週ごと</h2>${table([['label', '週'], ['views', '表示'], ['medianSeconds', '表示時間（中央値・秒）'], ['read75Pct', '75％以上読んだ（％）'], ['spPct', 'スマホ（％）'], ['backViews', '戻るで戻った'], ['outClicks', '外へのクリック'], ['deadClicks', 'リンクなしを押した'], ['rageClicks', '連打'], ['messageViews', 'フォームの表示'], ['formStarts', '入力を始めた'], ['formSubmits', '送信を押した'], ['formSubmitsValid', 'うち入力がそろっていた'], ['messagesReceived', '届いたメッセージ'], ['humanViews', '人'], ['botViews', '機械'], ['unknownViews', '操作なし'], ['unmarkedViews', '印なし']], byWeek)}
 <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)}
```

### 差分：README の説明（commit 5a5adaf）

```diff
commit 5a5adaf README に人と機械を分ける印の説明を足した

diff --git a/README.md b/README.md
index 9794128..723b26c 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,7 @@ Web改善の検証（Project の `claude/web_improvement_trial.md`）のため
 - 数は `/api/ev-stats`（週ごと・ページごと・ページからページへの移り方・どこから来たか・押したもの・押した場所・リンクでない所を押した）・`/api/ev-stats?format=json` で見る。`?week=週の月曜の日付` でその週だけ、`?since=日付` でその日以降だけ
 - 「届いたメッセージ」はフォームの処理には手を入れず、p2m-messages の一番上に保存された .json の数を数えている
 - エイイチや Claude の確認の閲覧を数に入れないため、どれかのページを `?p2m-notrack=1` を付けて開くと、そのブラウザでは以後送らない（`?p2m-notrack=0` で元に戻す。`?p2m-notrack=check` で、そのブラウザが今どちらの設定かが画面の上に出る）。iPhone の Safari は、サイトを7日開かないとこの設定を消すことがあるため、ときどき check で確かめる。Do Not Track・Global Privacy Control を設定したブラウザも送らない
+- 人と機械を分ける印（2026-10-25 から）：1回の表示ごとに `k`（h＝人の操作＝マウス・指・ホイール・キーがあった／b＝機械＝クローラーや自動のブラウザの名乗り・navigator.webdriver／x＝操作なし）を付ける。それより前の記録は「印なし」。週ごとの表に4つの内訳が出て、`?who=human`・`bot`・`unknown`・`old` でほかの表をその種類だけに絞れる。名乗り（ユーザーエージェント）は判定に使うだけで保存しない
 - 記録しない：入力した内容・コピーした文章・IP アドレス・ブラウザの種類・Cookie。番号はその1回の表示だけのもので、ブラウザに保存しない
 - 押したものの「ページのどの部分か」は、クラス名（.richText・.related・.consult など）で決めている。部品に `data-ev-area="名前"` を付けると、その名前で数える（名前は `worker/index.js` の `EV_AREAS` にあるものだけ受け付ける）。リンクやボタンに `data-ev="名前"` を付けると、行き先の前にその名前が付く
 
```

### 差分：llms.txt とクローラーの訪問の記録（commit e8ce84f）

```diff
commit e8ce84f llms.txt を置き、検索エンジン・AI のクローラーの訪問を数えるようにした

diff --git a/README.md b/README.md
index 723b26c..79dc599 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,13 @@ Web改善の検証（Project の `claude/web_improvement_trial.md`）のため
 記事・4コマの画像と動画は GitHub に入れない。ただし `public/og/` の画像は、GitHub にあるサムネイルの HTML から書き出したものなので GitHub に入れる。サイトの `/media/ファイル名` を `worker/index.js` が R2 から返す。
 メッセージフォームの送信は R2（p2m-messages）に保存する。フォームには「ご相談の種類」（フィジカルAI・ロボット／AI活用・仕組みづくり／取材・登壇／その他）があり、保存する JSON とメールの件名に入る。`/message?topic=ai` のように開くと、その種類を選んだ状態になる。値の一覧はフォーム（`src/pages/message.astro`）と `worker/index.js` の `TOPICS` の2か所にあるので、増やすときは両方を直す。
 
+## AI と検索エンジンのクローラー（2026-10-25）
+
+- `/llms.txt`：AI がこのサイトを読むときの案内。`src/pages/llms.txt.ts` がビルドのたびに、シリーズと記録の一覧（分けた記事は全文の Markdown つき）から作る
+- クローラーの訪問の記録：ページ上の計測（/api/ev）は JavaScript を動かさないクローラーを数えられないので、`worker/index.js` が配信のときに名乗り（GPTBot・ClaudeBot・PerplexityBot・Googlebot・bingbot など。一覧は `CRAWLERS`）で見分け、R2（p2m-messages の `cr/`）に「どのクローラーか・どのページか・返した結果・Cloudflare が本物と確かめたか」を残す。名乗りそのものは保存しない
+- そのため、ページ・テキストも worker を通す（`wrangler.jsonc` の `run_worker_first`。画像・CSS・JS・図解・試算の部品は `!` で外し、静的ファイルから直接返す）
+- 数は `/api/cr-stats`（鍵が要る。クローラーごと・週ごと・よく読まれたページ）・`?format=json`・`?since=日付`・`?bot=GPTBot`
+
 ## AI活用の記事と相談の案内
 
 AI活用の記事（フィジカルAIの研究を回すために作った仕組みの記録）は、シリーズ「ひとり研究所の裏側」（`series: "ひとり研究所の裏側"`・/series4。2026-10-17 に「ロボティクスデザイン実務フロー」から名前を変えた）に置く。「フィジカルAI研究室」は学習・推論を回す回だけにする。
diff --git a/wrangler.jsonc b/wrangler.jsonc
index c2b733b..afb413b 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -9,8 +9,9 @@
     "binding": "ASSETS",
     "not_found_handling": "404-page",
     "html_handling": "auto-trailing-slash",
-    // /media/ と /api/ と旧 URL の形（/pixelstomotors/…）は、静的ファイルより先に worker/index.js へ回す
-    "run_worker_first": ["/media/*", "/api/*", "/pixelstomotors", "/pixelstomotors/*"]
+    // ページ・テキストも worker/index.js を通す（検索エンジン・AI のクローラーの訪問を数えるため。2026-10-25）。
+    // 画像・CSS・JS・図解・試算の部品は数えないので、静的ファイルから直接返す（! が付いたもの）
+    "run_worker_first": ["/*", "!/_astro/*", "!/site/*", "!/og/*", "!/thumbs/*", "!/embeds/*", "!/sim/*", "!/favicon*"]
   },
   "r2_buckets": [
     // 記事・4コマの画像と動画（GitHub には入れない。tools/upload_media.py で Mac から入れる）
```

### コード：シリーズのページ src/pages/[series].astro

```astro
---
// シリーズのページ（/series1〜8）。上に Studio で置いていた資料（Figma・スプレッドシートなど）、下にそのシリーズの記録
import Base from '../layouts/Base.astro';
import RecordCard from '../components/RecordCard.astro';
import LoadMore from '../components/LoadMore.astro';
import GuideBanner from '../components/GuideBanner.astro';
import ThinkingBanner from '../components/ThinkingBanner.astro';
import ServingBanner from '../components/ServingBanner.astro';
import { seriesCards, SERIES_PAGES } from '../lib/records';
import { media } from '../lib/media';
import staticPages from '../data/static.json';
import { SITE, SITE_NAME } from '../lib/seo';

export function getStaticPaths() {
  return Object.keys(SERIES_PAGES).map((series) => ({ params: { series } }));
}
const { series } = Astro.params;
const conf = SERIES_PAGES[series];
const blocks = (staticPages[series]?.blocks || []).slice(1); // 先頭はページの見出し
const items = await seriesCards(conf.series);
const [enName, jaName] = conf.title.split(' | ');
// 見出しは日本語を先に置く（検索結果のタイトルが見出しに置き換えられるため。2026-10-25）
const heading = jaName ? `${jaName} | ${enName}` : conf.title;
const description = conf.intro ? `${SITE_NAME} のシリーズ「${jaName || conf.title}」（${enName}）。${conf.intro}` : `${SITE_NAME} のシリーズ「${jaName || conf.title}」（${enName}）の記録一覧。ロボットとフィジカルAIを、29年のデザイナー経験から実機とシミュレーションで試した過程を、失敗も含めて公開しています。`;
const jsonLd = {
  '@context': 'https://schema.org', '@type': 'CollectionPage', name: heading, description, url: `${SITE}/${series}`,
  inLanguage: 'ja',
  hasPart: items.slice(0, 50).map((c) => ({ '@type': 'BlogPosting', headline: c.title, url: `${SITE}/record/${c.slug}` })),
};
---
<Base title={`${jaName || conf.title}（${enName}）| ${SITE_NAME}`} description={description} path={`/${series}`} jsonLd={jsonLd}>
  <section class="s-hero"><div class="panel">
    <h1 class="sec-h s-title">{heading}</h1>
    {conf.intro && <p class="s-intro">{conf.intro}</p>}
    {conf.points && (
      <div class="s-points">
        {conf.points.map((p) => (
          <div><h2 class="s-points-h">{p.h}</h2><ul class="s-obs-list">{p.items.map((t) => <li>{t}</li>)}</ul></div>
        ))}
      </div>
    )}
    {conf.guide && <div class="s-guide"><GuideBanner /></div>}
    {conf.thinking && <div class="s-guide"><ThinkingBanner /></div>}
    {conf.serving && <div class="s-guide"><ServingBanner /></div>}
    {blocks.map((b) => (
      b.type === 'text' ? (b.link ? <a class="s-label" href={b.link} target={b.link.startsWith('/') ? undefined : '_blank'} rel={b.link.startsWith('/') ? undefined : 'noopener'} set:html={b.html} /> : <p class="s-label" set:html={b.html} />)
      : b.type === 'iframe' ? <div class="s-frame" style={`height:${b.height}px`} set:html={b.html} />
      : b.type === 'embed' ? <div class="s-embed" set:html={b.html} />
      : b.type === 'img' ? (b.link ? <a class="s-img" href={b.link} target="_blank" rel="noopener"><img src={media(b.src)} alt={b.alt || conf.title} loading="lazy" /></a> : <div class="s-img"><img src={media(b.src)} alt={b.alt || conf.title} loading="lazy" /></div>)
      : null
    ))}
  </div></section>
  {conf.sim && (
    <section class="sec s-sim" data-ev-area="sim"><div class="panel">
      <h2 class="sec-h">試算を試す | Simulator</h2>
      {conf.sim.tabs && (
        <div class="s-tabs" role="tablist" aria-label="業種とロボットの仕事">
          {conf.sim.tabs.map((t, i) => (
            <button type="button" role="tab" class={`s-tab${t.img ? ' has-img' : ''}`} id={`simtab-${t.id}`} aria-selected={i === 0 ? 'true' : 'false'} aria-controls="sim-panel"
              data-tab={t.id} data-src={t.src} data-title={t.title || conf.sim.title} data-note={t.note || conf.sim.note} data-ev={`試算のタブ:${t.task}｜${t.place}`}>
              {t.img && <img src={t.img} alt="" loading="lazy" />}
              <b>{t.task}</b><span>{t.place.split('・').map((w, k, all) => <i>{w}{k < all.length - 1 ? '・' : ''}</i>)}</span>
            </button>
          ))}
        </div>
      )}
      <div id="sim-panel" role={conf.sim.tabs ? 'tabpanel' : undefined}>
        <p class="s-sim-title" id="sim-title">{conf.sim.title}</p>
        <p class="s-sim-note" id="sim-note">{conf.sim.note}</p>
        {conf.sim.obs && <a class="s-obs-jump" id="sim-obs-jump" href="#sim-obs" data-ev="観察シートへ移る">止まった理由・機種・場所を店で記録する観察シートは、試算の下にあります ↓</a>}
        <iframe data-embed id="sim-frame" src={`${conf.sim.src}?embed=1`} title={conf.sim.title} class="s-sim-frame" loading="lazy"></iframe>
        <a class="s-sim-open" id="sim-open" href={conf.sim.src} target="_blank" rel="noopener" data-ev="試算を全画面で開く">全画面で開く ↗</a>
      </div>
      {conf.sim.obs && (
        <div class="s-obs" id="sim-obs">
          <div class="s-obs-text">
            <h3 class="s-obs-h">{conf.sim.obs.title}</h3>
            <p class="s-sim-note">{conf.sim.obs.note}</p>
            <ul class="s-obs-list">{conf.sim.obs.points.map((t) => <li>{t}</li>)}</ul>
            <a class="s-obs-btn" href={conf.sim.obs.src} target="_blank" rel="noopener" data-ev="観察シートの例を開く">{conf.sim.obs.label}</a>
          </div>
          <div class="s-obs-phone">
            <iframe src={`${conf.sim.obs.src}?preview=1`} title={conf.sim.obs.title} loading="lazy"></iframe>
            <p class="s-obs-cap">スマホの幅で表示しています。中のボタンも押せます</p>
          </div>
        </div>
      )}
    </div></section>
  )}
  <script>
    // 試算のタブ：押すと中の試算を切り替える。/series8#sim-hospital のように開くと、そのタブから始まる
    const tabs = [...document.querySelectorAll<HTMLButtonElement>('.s-tab')];
    const frame = document.getElementById('sim-frame') as HTMLIFrameElement | null;
    if (tabs.length && frame) {
      const title = document.getElementById('sim-title')!, note = document.getElementById('sim-note')!;
      const open = document.getElementById('sim-open') as HTMLAnchorElement, obs = document.getElementById('sim-obs');
      const embedUrl = (src: string) => { const [path, hash] = src.split('#'); return `${path}?embed=1${hash ? '#' + hash : ''}`; };
      // 中の高さに合わせて枠の高さを変える（同じサイトのページなので中を読める）
      let ro: ResizeObserver | null = null;
      const fit = () => {
        try {
          const d = frame.contentDocument; if (!d || !d.body) return;
          // html の高さは枠の高さより小さくならないので、body の高さで測る
          const cs = getComputedStyle(d.body);
          frame.style.height = Math.ceil(d.body.getBoundingClientRect().height + parseFloat(cs.marginTop) + parseFloat(cs.marginBottom)) + 'px';
        } catch {}
      };
      frame.addEventListener('load', () => {
        fit(); ro?.disconnect();
        try { ro = new ResizeObserver(fit); ro.observe(frame.contentDocument!.body); } catch {}
      });
      const select = (t: HTMLButtonElement, push: boolean) => {
        tabs.forEach((b) => { const on = b === t; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });
        const src = t.dataset.src!;
        title.textContent = t.dataset.title!; note.textContent = t.dataset.note!;
        frame.title = t.dataset.title!; open.href = src;
        const next = embedUrl(src);
        const cur = frame.getAttribute('src') || '';
        if (cur !== next) {
          frame.setAttribute('src', next);
          // 同じページで # だけが違うとき、ブラウザによっては src を変えても中が切り替わらないので、中の # も直接変える
          const [np, nh] = next.split('#');
          if (cur.split('#')[0] === np && nh) { try { frame.contentWindow!.location.hash = nh; } catch {} }
        }
        if (obs) obs.hidden = t !== tabs[0];
        const jump = document.getElementById('sim-obs-jump'); if (jump) jump.hidden = t !== tabs[0];
        // スマホでは選んだタブが見える位置まで、タブの列を横に送る
        const strip = t.parentElement!; strip.scrollTo({ left: Math.max(t.offsetLeft - 8, 0), behavior: push ? 'smooth' : 'auto' });
        if (push) history.replaceState(null, '', t === tabs[0] ? location.pathname : `#sim-${t.dataset.tab}`);
      };
      tabs.forEach((t, i) => {
        t.tabIndex = i === 0 ? 0 : -1;
        t.addEventListener('click', () => select(t, true));
        t.addEventListener('keydown', (e) => {
          const k = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
          if (!k) return;
          e.preventDefault();
          const n = tabs[(i + k + tabs.length) % tabs.length]; n.focus(); select(n, true);
        });
      });
      const start = tabs.find((t) => location.hash === `#sim-${t.dataset.tab}`);
      if (start) { select(start, false); document.querySelector('.s-sim')?.scrollIntoView(); }
    }
  </script>
  <section class="sec"><div class="panel">
    <h2 class="sec-h">記録 | Record</h2>
    <LoadMore limit={conf.limit} total={items.length}>{items.map((c) => <RecordCard item={c} />)}</LoadMore>
  </div></section>
</Base>
<style>
  .s-hero { padding: 88px 80px 0; }
  .s-title { margin: 0; }
  .s-guide { margin: 24px 0 0; }
  .s-intro { margin: 16px 0 0; font: 400 16px/28px var(--sans); color: var(--ink-2); }
  .s-points { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; margin: 24px 0 0; }
  .s-points-h { margin: 0 0 8px; font: 600 16px/26px var(--sans); color: var(--ink); }
  .s-points .s-obs-list { margin: 0; }
  @media (max-width: 767px) { .s-points { grid-template-columns: minmax(0, 1fr); gap: 20px; } }
  .s-label { display: block; margin: 40px 0 16px; font: 500 16px/24px var(--sans); color: var(--ink); }
  .s-title + .s-label { margin-top: 8px; }
  a.s-label:hover { text-decoration: underline; }
  .s-frame { width: 100%; }
  .s-frame :global(iframe) { display: block; width: 100% !important; height: 100% !important; }
  .s-embed :global(.embed iframe) { display: block; width: 100%; height: 150px; border: 0; }
  .s-img img { width: 100%; height: auto; display: block; }
  .s-sim-title { margin: -12px 0 8px; font: 600 18px/28px var(--sans); color: var(--ink); }
  .s-sim-note { margin: 0 0 20px; font: 400 14px/24px var(--sans); color: var(--ink-2); }
  .s-sim-frame { display: block; width: 100%; height: 1600px; border: 0; }
  .s-tabs { position: relative; display: flex; gap: 8px; margin: -8px 0 28px; overflow-x: auto; padding: 8px; background: #111; border-radius: 12px; scrollbar-width: thin; }
  .s-tab { flex: 1 0 140px; display: grid; gap: 2px; align-content: start; text-align: left; padding: 12px 14px; border: 0; border-radius: 8px; background: #2a2a2a; color: #f5f5f0; cursor: pointer; font: inherit; }
  .s-tab img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 4px; margin-bottom: 8px; opacity: .78; transition: opacity .2s; }
  .s-tab:hover img, .s-tab[aria-selected="true"] img { opacity: 1; }
  .s-tab.has-img { padding: 8px 8px 12px; }
  .s-tab b { font: 600 16px/24px var(--sans); }
  .s-tab span { font: 400 12px/18px var(--sans); color: #b8b8b0; }
  .s-tab span i { font-style: normal; display: inline-block; } /* 「商業施設・ビル」は、折り返すなら・のあとで折り返す */
  .s-tab:hover { background: #3a3a3a; }
  .s-tab[aria-selected="true"] { background: #f5f5f0; color: #111; }
  .s-tab[aria-selected="true"] span { color: #555; }
  .s-tab:focus-visible { outline: 2px solid #f5f5f0; outline-offset: 2px; }
  /* スマホでは横スクロールにせず、2列に並べて6つとも見せる */
  @media (max-width: 599px) {
    .s-tabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); overflow: visible; gap: 6px; padding: 6px; }
    /* 写真を左に、文字を右に置いて、カードの高さを抑える。カードの余白はなくし、写真をカードの端まで広げる */
    .s-tab, .s-tab.has-img { grid-template-columns: 72px minmax(0, 1fr); grid-template-rows: 1fr 1fr; align-content: stretch; column-gap: 8px; row-gap: 0; align-items: center; padding: 0; overflow: hidden; min-height: 58px; }
    .s-tab img { grid-row: 1 / span 2; align-self: stretch; width: 100%; height: 100%; aspect-ratio: auto; margin: 0; border-radius: 0; }
    .s-tab b { font-size: 15px; line-height: 20px; align-self: end; padding: 8px 4px 0 0; }
    .s-tab span { font-size: 11px; line-height: 16px; align-self: start; padding: 0 4px 8px 0; }
  }
  .s-sim-open { display: inline-block; margin-top: 16px; font: 500 14px/22px var(--sans); color: var(--ink); }
  .s-sim-open:hover { text-decoration: underline; }
  .s-obs { margin-top: 40px; padding-top: 40px; border-top: 1px solid var(--line, #e6e6e6); display: grid; grid-template-columns: minmax(0, 1fr) 390px; gap: 48px; align-items: start; }
  .s-obs[hidden], .s-obs-jump[hidden] { display: none; }
  .s-obs-jump { display: inline-block; margin: -8px 0 20px; font: 500 14px/22px var(--sans); color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
  .s-obs { scroll-margin-top: 96px; }
  .s-obs-h { margin: 0 0 8px; font: 600 18px/28px var(--sans); color: var(--ink); }
  .s-obs .s-sim-note { margin: 0 0 16px; }
  .s-obs-list { margin: 0 0 24px; padding-left: 1.2em; font: 400 14px/26px var(--sans); color: var(--ink-2); }
  .s-obs-btn { display: inline-flex; align-items: center; gap: 6px; padding: 12px 20px; border-radius: 8px; background: var(--ink, #242424); color: #fff; font: 600 14px/20px var(--sans); }
  .s-obs-btn:hover { opacity: .85; }
  .s-obs-phone iframe { display: block; width: 390px; max-width: 100%; height: 720px; border: 10px solid #242424; border-radius: 32px; background: #f7f7f7; }
  .s-obs-cap { margin: 8px 0 0; text-align: center; font: 400 12px/18px var(--sans); color: var(--ink-2); }
  @media (max-width: 1023px) { .s-obs { grid-template-columns: minmax(0, 1fr); gap: 24px; } .s-obs-phone { justify-self: center; width: 100%; max-width: 390px; } .s-obs-phone iframe { height: 640px; } }
  @media (max-width: 1023px) { .s-hero { padding: 72px 24px 0; } }
  @media (max-width: 599px) { .s-hero { padding: 64px 12px 0; } }
</style>
```

### コード：シリーズの設定 src/lib/records.ts

```ts
import { getCollection } from 'astro:content';
import cards from '../data/cards.json';
import mangaCards from '../data/mangaCards.json';
import englishCards from '../data/englishCards.json';

export type Card = { day: string; series: string; published: string; title: string; slug: string; cover: string | null; thumb: string | null };
export type MangaCard = { slug: string; title: string; label: string; character: string; description: string; published: string; cover: string; comic: string; cover_alt: string; comic_alt: string };

// 一覧に並べる記事：Claude が書いた Markdown の記事 ＋ Studio から移した記事
// 並び順は LOG 番号の大きい順（1日に何本も出すようになり、公開日時の順だと番号と前後することがあるため）。
// 番号が同じか取れないもの（英語版の LOG187EN など）は公開日時で並べる
export async function allCards(): Promise<Card[]> {
  const md = (await getCollection('records')).map((e) => ({
    day: `LOG${e.data.day}`, series: e.data.series, published: e.data.date.toISOString(),
    title: e.data.title, slug: e.data.slug, cover: e.data.cover ?? null, thumb: e.data.cover ? null : `/thumbs/LOG${e.data.day}.html`,
  }));
  const seen = new Set(md.map((c) => c.slug));
  const studio = (cards as Card[]).filter((c) => !seen.has(c.slug));
  const num = (c: Card) => { const m = /^LOG(\d+)/.exec(c.day); return m ? Number(m[1]) : -1; };
  return [...md, ...studio].sort((a, b) => (num(b) - num(a)) || b.published.localeCompare(a.published));
}

export async function seriesCards(series: string): Promise<Card[]> {
  return (await allCards()).filter((c) => c.series === series);
}

export const allManga = mangaCards as MangaCard[];
export const allEnglish = englishCards as MangaCard[];

// 試算のタブ（シリーズのページの Simulator）。task は何をするロボットか、place はどこで使うか。
// 1つ目は sim.src・sim.title・sim.note・sim.obs をそのまま使う。2つ目からは src に # で現場を付ける
// img はタブに出す写真（public/site/sim/ に置く。なければ文字だけ）
export type SimTab = { id: string; task: string; place: string; src: string; title: string; note: string; img?: string };

// シリーズのページ（URL → 記事のシリーズ名）。Studio の一覧の絞り込み条件と同じ
// intro はシリーズのページの見出しの下に出す説明（ないシリーズは出さない）
export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number; intro?: string; points?: { h: string; items: string[] }[]; guide?: boolean; thinking?: boolean; serving?: boolean; sim?: { src: string; title: string; note: string; obs?: { src: string; title: string; label: string; note: string; points: string[] }; tabs?: SimTab[] } }> = {
  series1: { series: 'HRI', title: 'Drug Delivery Robot | 薬剤搬送ロボットハコブくん', pageTitle: 'Series1 | 薬剤搬送ロボット', limit: 12,
    intro: '病院で薬を運ぶロボット「ハコブくん」を題材に、人とロボットの関わり（HRI）を設計している記録です。病院で使う画面（UI）の設計、Unity での自律移動、Fusion での形づくり、分身ロボット（アバター）の考察までを、4コママンガと一緒に残しています。' },
  series2: { series: 'R2R2R', title: 'R2R2R | デジタルツイン学習-R2R2R（Real to Render to Real）', pageTitle: 'Series2 | R2R2R', limit: 12,
    intro: 'iPhone で物を3Dスキャンし、Unity の中で動きを試してから、机の上のロボットアーム（mechArm 270）で実際に動かす記録です。ロボット工学を専門に学んでいないデザイナーでも、アームのつかむ・渡すといった動きを設計できるかを、失敗も含めて残しています。' },
  series3: { series: 'UV-C農業ロボット', title: 'UV-C Agricultural Robot | UV-C農業ロボット', pageTitle: 'Series3 | UV-C農業ロボット', limit: 20,
    intro: '湿気の多い日本の農業で、紫外線（UV-C）を当てて作物の病気を抑えるロボットが何を変えるかを考えた記録です。フィジカルAIを使った農業を、製造業のように管理する仕組みとして捉え直しています。' },
  series4: { series: 'ひとり研究所の裏側', title: 'Behind the Solo Lab | ひとり研究所の裏側', pageTitle: 'Series4 | ひとり研究所の裏側', limit: 12,
    intro: 'フィジカルAIの研究をひとりで続けるために、AIと一緒に作ってきた仕組みの記録です。サイトの移行、動画の制作、記録の整理など、研究の外側の作業をどう回しているかを残しています。' },
  series5: { series: '考察記事', title: 'Deep Dive | 考察記事', pageTitle: 'Series5 | 考察記事', limit: 12, thinking: true,
    intro: '実装の記録とは別に、フィジカルAIやロボットについて、なぜそうなるのか、何を意味するのかを考えた記事です。論文や企業の発表、市場の動きを読み、対話の形で考えたことを、外れた議論も含めてそのまま残しています。' },
  series6: { series: 'フィジカルAI研究室', title: 'Physical AI Lab | フィジカルAI研究室', pageTitle: 'Series6 | フィジカルAI研究室', limit: 12, guide: true,
    intro: 'ロボットアームが動きを覚える環境を、自宅にひとりで作っている記録です。机の上のアームでお手本の動きを集め、ロボット用のAI（Physical Intelligence の π0.5 など）に学習させ、実機で動かして確かめます。借りたGPUの種類・時間・費用も含めて、誰でも同じ手順で始められる形で残しています。' },
  series7: { series: '画像認識・物体検出', title: 'Image recognition and Object detection | 画像認識と物体検出', pageTitle: 'Series7 | 画像認識と物体検出', limit: 12,
    intro: 'ロボットのカメラが物を見つける仕組みを、下の層から学び直している記録です。画素の数字、色の見分け方、しきい値、輪郭、特徴点といった昔からの手法を、机の上のアームを撮った写真で1つずつ測って確かめ、その上で SAM 3 のような新しいモデルも試します。評価の物差し（IoU・適合率・再現率など）を先に決め、結果は数字で残しています。' },
  series8: { series: '導入シミュレーション', title: 'Service Robot Simulation | サービスロボット導入シミュレーション', pageTitle: 'Series8 | サービスロボット導入シミュレーション', limit: 12, serving: true,
    intro: 'サービスロボット導入シミュレーションとは、配膳・清掃・搬送などのロボットを店や施設に入れる前に、待ち時間・必要な人数・ロボットが止まる場所・費用を試算して確かめることです。このシリーズでは、時間・順番・台数を SimPy で、ぶつかる・こぼれるといった空間と動きを Unity で扱い、結果とコードを全量公開しています。',
    // 見出しの下に並べる短い一覧（検索や AI が「何が分かるか・どう試算するか」を拾えるように、本文の文字で書く）
    points: [
      { h: 'シミュレーションで分かること', items: ['必要な人数：ロボットを何台入れれば、スタッフを何人減らせるか', '待ち時間：ピーク時の入店待ちと、料理が届くまでの時間', '止まる場所：通路や席の配置で、ロボットが詰まる箇所', '費用：ロボットの月額と、浮く人件費の比較（補助金を含む）'] },
      { h: '試算の方法', items: ['時間・順番・台数：離散イベントシミュレーション（SimPy）', '空間と動き：3Dシミュレーション（Unity）', '前提の数字：公開事例と、店で数える観察シート', 'ブラウザで試せる試算：飲食店・病院・商業施設・ホテル・オフィス・屋外の6現場'] },
    ],
    sim: { src: '/sim/serving-robot.html', title: '配膳ロボット、何台入れたら店は変わる？',
      note: '店を選び、人数・台数・ロボットの使い方を切り替えると、ピーク時の入店待ちや料理が届くまでの時間が変わります。標準ファミレスは、店の形（平面図）から選ぶこともできます。レジの数（1つ・2つ）も選べます。ロボットの費用と浮く人件費も比べられます（補助金の情報つき）。1つの条件を40回回した平均で、数字はすべて仮の前提による試算です（試作 v2.8・32,850条件）。',
      obs: { src: '/sim/serving-robot-observation.html', title: '試算の仮の数字を、店で数える観察シート', label: '観察シートを開く ↗',
        note: '配膳ロボットが通路で何回止まるかは、公開データが見つかりませんでした。店で片手で押して数えるための記録シートの例です。押した記録はこのブラウザの中にだけ保存され、どこにも送られません。',
        points: ['ロボットの出発・卓に着いた・止まった・戻ったを押すと、1往復の時間と止まった割合が出る', '止まった場所と原因を選べる。選択肢は記録する人の立場（客席から・店のスタッフ・導入や保守の担当）で切り替わる', '地図やルートを変える前と後の観察を並べて比べられる'] },
      tabs: [
        { id: 'serving', img: '/site/sim/serving.jpg?v=2', task: '配膳', place: '飲食店', src: '/sim/serving-robot.html', title: '', note: '' },
        { id: 'hospital', img: '/site/sim/hospital.jpg?v=1', task: '搬送', place: '病院', src: '/sim/service-robot-sites.html#hospital', title: '検体と薬剤の搬送、ロボットに任せると何が変わる？',
          note: '慈恵医大柏病院の公開事例の形（3つのルート・運行の時間帯・同じ階）をもとに、病院の広さを選び、人が運ぶ時間と、荷物が届くまでの時間を比べます。ロボットが途中で止まる割合など、公開されていない値は仮置きで、何段階か動かせます（試作 v1・3,276条件・各30回の平均）。' },
        { id: 'cleaning', img: '/site/sim/cleaning.jpg?v=2', task: '清掃', place: '商業施設・ビル', src: '/sim/service-robot-sites.html#cleaning', title: '夜間の床清掃、人の作業はどれだけ減る？',
          note: '施設の広さ・ロボットが拭ける広い床の割合・台数・引っかかる回数を切り替えると、人の作業時間と、ロボットに呼ばれる回数が変わります。施設全体で5%減と対象エリアで60%減という公開事例の差を、床の割合で確かめられます（試作 v1・5,840条件・各30回の平均）。' },
        { id: 'hotel', img: '/site/sim/hotel.jpg?v=2', task: '配送', place: 'ホテル', src: '/sim/service-robot-sites.html#hotel', title: 'アメニティの客室配送、フロントは楽になる？',
          note: 'ホテルの広さ（120室・200室・400室）を選び、客室からの依頼をロボットで届けると、フロントを離れる時間・届くまでの時間・チェックインの待ちがどう変わるかを比べます（試作 v1・2,379条件・各30回の平均）。' },
        { id: 'security', img: '/site/sim/security.jpg?v=1', task: '巡回', place: 'オフィスビル', src: '/sim/service-robot-sites.html#security', title: '夜間の警備、巡回をロボットに任せると？',
          note: 'ビルの広さを選び、ロボットが巡回して異常を知らせ、警備員が駆けつける形で、異常に人が着くまでの時間と、防災センターが空く時間を比べます。異常でないものを知らせる空振りの回数は仮置きです（試作 v1・1,674条件・各30回の平均）。' },
        { id: 'outdoor', img: '/site/sim/outdoor.jpg?v=2', task: '配送', place: '屋外（歩道）', src: '/sim/service-robot-sites.html#outdoor', title: '歩道の配送ロボット、何件から人が足りなくなる？',
          note: '店から家へ届けるロボットと遠隔のオペレーターで、配達エリアの広さ・注文の数・台数・1人が見られる台数を切り替えます。止まってオペレーターを呼ぶ回数は仮置きです（試作 v1・3,960条件・各30回の平均）。' },
      ] } },
};
```

### コード：サイトの使われ方の記録 src/components/SiteMeasure.astro

```astro
---
// サイトの使われ方の記録（2026-10-18）。Web改善の検証（Project の claude/web_improvement_trial.md）のため、
// Cloudflare Web Analytics では取れない「どこまで読んだか・何を押したか・どこで迷ったか」を数える。
// 最初は広く取り、データがたまってから見るものを絞る方針。
//
// 1回のページ表示 = 1件。ページが見えなくなったとき（閉じる・移る・タブを切り替える）に、まとめて /api/ev へ送る。
// 戻ってきてまた見えなくなったら、同じ番号で中身を足して送り直す（数えるときは番号ごとに最後の1件だけ使う）。
//
// 送るもの：ページ・サイト内のどのページから来たか（戻るボタンで戻ったか）・外から来たならそのサイトの名前（ドメイン）だけ・
// 画面の幅（pc/sp）・どこまで読んだか・表示していた時間・押したリンクやボタン（行き先とページのどの部分か）・
// リンクでない所を押した回数・短い間に同じ所を何度も押した回数・コピーした回数・動画を再生した回数・
// フォームに入力を始めたか・送信ボタンを押した回数・
// 人が操作したか（マウスを動かす・押す・指で触れる・ホイール・キーのどれかがあったか）・
// 自動で動くブラウザか（navigator.webdriver と、ブラウザの名乗りに Headless・bot などが入っているかの判定結果だけ）
// 送らないもの：入力した内容・コピーした文章・IP アドレス・ブラウザの種類・Cookie。
// 番号はこの1回の表示のためだけに作り、ブラウザに保存しない（別のページや次の来訪とはつながらない）。
//
// 送らない場合：
// - 「トラッキングしないで」（Do Not Track / Global Privacy Control）を設定したブラウザ
// - エイイチ自身の閲覧：どれかのページを ?p2m-notrack=1 を付けて開くと、そのブラウザでは以後送らない（=0 で元に戻す。=check で今の設定を表示）
// 数は /api/ev-stats（表）・/api/ev-stats?format=json で見る
---
<script>
  type Click = [string, string];
  const OPT = 'p2m-notrack';
  let off = false;
  let q: string | null = null;
  let saved = true;
  try {
    q = new URLSearchParams(location.search).get(OPT);
    if (q === '1') localStorage.setItem(OPT, '1');
    if (q === '0') localStorage.removeItem(OPT);
    off = localStorage.getItem(OPT) === '1';
  } catch { saved = false; /* 覚えられない環境では、送るかどうかを毎回の判定だけで決める */ }
  const nav = navigator as Navigator & { globalPrivacyControl?: boolean };
  const dnt = nav.doNotTrack === '1' || nav.globalPrivacyControl === true;
  if (dnt) off = true;
  // ?p2m-notrack=1 / 0 / check で開いたときだけ、このブラウザが今どちらの設定かを画面の上に6秒出す（設定できたかの確認用）
  if (q === '1' || q === '0' || q === 'check') {
    const note = document.createElement('div');
    note.textContent = dnt ? 'このブラウザは「トラッキングしない」設定のため記録しません'
      : !saved ? 'このブラウザでは設定を覚えられません（プライベートブラウズなど）'
      : off ? 'このブラウザの閲覧は記録しない設定です' : 'このブラウザの閲覧は記録する設定です';
    note.setAttribute('role', 'status');
    note.style.cssText = 'position:fixed;top:64px;left:50%;transform:translateX(-50%);z-index:100;width:max-content;max-width:calc(100% - 32px);padding:10px 16px;border-radius:8px;background:#242424;color:#fff;font:500 14px/1.5 sans-serif;box-shadow:0 4px 12px rgba(0,0,0,.2)';
    const show = () => { document.body.appendChild(note); setTimeout(() => note.remove(), 6000); };
    if (document.body) show(); else document.addEventListener('DOMContentLoaded', show);
  }

  if (!off) {
    const AUTO_UA = /headless|lighthouse|bot\/|bot;|\bbot\b|crawl|spider|slurp|preview|phantom|puppeteer|playwright|selenium/i;
    const newId = () => {
      try { return crypto.randomUUID().replace(/-/g, '').slice(0, 12); } catch { return Math.random().toString(36).slice(2, 14); }
    };
    // サイト内のどこから来たか。外から来たときは、そのサイトの名前（ドメイン）だけ
    const origin = () => {
      try {
        if (!document.referrer) return { f: '', r: '' };
        const u = new URL(document.referrer);
        if (u.host === location.host) return { f: u.pathname.slice(0, 160), r: '' };
        return { f: '', r: u.hostname.slice(0, 80) };
      } catch { return { f: '', r: '' }; }
    };
    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,
        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＝自動で動くブラウザの印（名乗りそのものは送らない）
        u: 0, wd: navigator.webdriver ? 1 : 0, hl: AUTO_UA.test(navigator.userAgent || '') ? 1 : 0,
      };
    };
    let st = fresh();
    // 人の操作。1回あれば十分なので、最初の1回だけ印を付ける
    const touched = () => { if (!st.u) { st.u = 1; dirty = true; } };
    for (const ev of ['pointermove', 'pointerdown', 'touchstart', 'wheel', 'keydown']) document.addEventListener(ev, touched, { capture: true, passive: true });
    let visibleSince = document.hidden ? 0 : Date.now();
    let dirty = true;

    // どこまで読んだか（25％刻み）。画面に収まるページは最後まで読んだ扱い
    const depth = () => {
      const h = document.documentElement.scrollHeight;
      if (!h) return;
      const ratio = Math.min(1, (window.scrollY + window.innerHeight) / h);
      const b = ratio >= 0.98 ? 100 : Math.floor(ratio * 4) * 25;
      if (b > st.s) { st.s = b; dirty = true; }
    };
    window.addEventListener('scroll', depth, { passive: true });
    window.addEventListener('load', depth);
    setTimeout(depth, 1500);

    // ページのどの部分か
    const AREAS: [string, string][] = [
      ['.hk-parade', 'hakobu'], ['.site-header', 'header'], ['.site-footer', 'footer'], ['.crumbs', 'crumbs'],
      ['.toc', 'toc'], ['.consult', 'consult'], ['.pager', 'pager'], ['.related', 'related'], ['.richText', 'body'], ['main', 'main'],
    ];
    const areaOf = (el: Element) => {
      const tagged = el.closest('[data-ev-area]') as HTMLElement | null;
      if (tagged && tagged.dataset.evArea) return tagged.dataset.evArea.slice(0, 20);
      for (const [sel, name] of AREAS) if (el.closest(sel)) return name;
      return 'other';
    };
    const addClick = (target: string, area: string) => {
      if (st.c.length < 8) st.c.push([target.slice(0, 100), area]);
      dirty = true;
    };
    // ボタンの文字（アイコンの文字 .ms は除く）
    const textOf = (el: Element) => {
      const aria = el.getAttribute('aria-label');
      if (aria) return aria.slice(0, 24);
      const c = el.cloneNode(true) as Element;
      c.querySelectorAll('.ms').forEach((i) => i.remove());
      return (c.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 24);
    };

    // 短い間に同じ所を何度も押した（3回以上・0.8秒以内・30px 以内）
    let taps: { x: number; y: number; at: number }[] = [];
    const INTERACTIVE = 'a,button,input,select,textarea,label,summary,video,audio,iframe,[role=button],[role=link],[onclick],[tabindex]';

    document.addEventListener('click', (e) => {
      try {
        const el = e.target as Element | null;
        if (!el || !(el instanceof Element)) return;
        const now = Date.now();
        taps = taps.filter((t) => now - t.at < 800 && Math.abs(t.x - e.clientX) < 30 && Math.abs(t.y - e.clientY) < 30);
        taps.push({ x: e.clientX, y: e.clientY, at: now });
        if (taps.length === 3) { st.g++; dirty = true; }

        const link = el.closest('a[href]') as HTMLAnchorElement | null;
        if (link) {
          const label = link.dataset.ev;
          let target = '';
          try {
            const u = new URL(link.href, location.href);
            if (u.protocol === 'mailto:' || u.protocol === 'tel:') target = 'mailto';
            else if (u.host !== location.host) target = u.hostname;
            else if (u.pathname === location.pathname && u.hash) target = '#' + decodeURIComponent(u.hash.slice(1)).slice(0, 40);
            else target = u.pathname + (u.search.includes('topic=') ? u.search : '');
          } catch { target = '(読めないリンク)'; }
          addClick(label ? `${label} ${target}` : target, areaOf(link));
          return;
        }
        const btn = el.closest('button,[role=button],summary') as HTMLElement | null;
        if (btn) { addClick('btn:' + (btn.dataset.ev || btn.id || textOf(btn) || btn.tagName.toLowerCase()), areaOf(btn)); return; }
        if (el.closest(INTERACTIVE)) return;
        // ページの余白（html・body・main そのもの）を押したのは迷いに数えない
        if (el === document.documentElement || el === document.body || el.tagName === 'MAIN') return;
        // リンクでない所を押した（文字を選んでいるときは除く）
        const sel = window.getSelection();
        if (sel && String(sel).length > 0) return;
        st.d++;
        addClick('(リンクなし):' + el.tagName.toLowerCase(), areaOf(el));
      } catch { /* 数え損ねてもサイトの動きには影響させない */ }
    }, { capture: true, passive: true });

    // 埋め込み（図解・YouTube）を押したとき：ページから枠の中は見えないので、枠に入ったことで数える
    window.addEventListener('blur', () => {
      setTimeout(() => {
        const a = document.activeElement;
        if (a && a.tagName === 'IFRAME') {
          let target = 'iframe';
          try {
            const u = new URL((a as HTMLIFrameElement).src, location.href);
            target = 'iframe:' + (u.host === location.host ? u.pathname.slice(0, 60) : u.hostname);
          } catch { /* src が読めないときは iframe とだけ残す */ }
          addClick(target, areaOf(a));
        }
      }, 0);
    });

    document.addEventListener('copy', () => { st.cp++; dirty = true; });
    // 動画の再生は、人が押して再生したときだけ数える（自動再生は数えない）。
    // 自動再生の動画は、その動画を直前（1.5秒以内）に押したときだけ数える
    let pressed: { el: Element | null; at: number } = { el: null, at: 0 };
    const markPress = (e: Event) => { pressed = { el: e.target as Element | null, at: Date.now() }; };
    document.addEventListener('pointerdown', markPress, { capture: true, passive: true });
    document.addEventListener('keydown', markPress, { capture: true, passive: true });
    document.addEventListener('play', (e) => {
      const v = e.target as HTMLMediaElement | null;
      if (!v || !(v instanceof HTMLMediaElement)) return;
      const byPress = !!pressed.el && (pressed.el === v || v.contains(pressed.el)) && Date.now() - pressed.at < 1500;
      if (v.autoplay && !byPress) return;
      st.vp++; dirty = true;
    }, true);
    document.addEventListener('focusin', (e) => {
      if (!st.fs && (e.target as Element | null)?.closest?.('form')) { st.fs = 1; dirty = true; }
    });
    document.addEventListener('submit', (e) => {
      st.fx++;
      try { if ((e.target as HTMLFormElement).checkValidity()) st.fv++; } catch { /* 判定できないときは押した回数だけ */ }
      dirty = true;
    }, true);

    const send = () => {
      if (visibleSince) { st.t += Math.round((Date.now() - visibleSince) / 1000); visibleSince = 0; }
      st.t = Math.min(st.t, 7200);
      if (!dirty) return;
      depth();
      const body = JSON.stringify(st);
      try {
        if (!navigator.sendBeacon || !navigator.sendBeacon('/api/ev', new Blob([body], { type: 'text/plain' }))) {
          fetch('/api/ev', { method: 'POST', body, keepalive: true }).catch(() => {});
        }
      } catch { /* 送れなくてもサイトの動きには影響させない */ }
      st.n++; dirty = false;
    };
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) send();
      else visibleSince = Date.now();
    });
    window.addEventListener('pagehide', send);
    // 戻るボタンで、前の表示のまま戻ってきたとき：別の1回として数え直す
    window.addEventListener('pageshow', (e) => {
      if (e.persisted) { st = fresh('(戻る)'); visibleSince = Date.now(); dirty = true; setTimeout(depth, 300); }
    });
  }
</script>
```

### コード：配信とクローラーの記録・集計 worker/index.js

```js
// サイトの入口。ほとんどのページは作っておいた静的ファイル（dist/）をそのまま返す。
// ここで処理するのは3つだけ:
// - /pixelstomotors/…   旧 URL の形。/pixelstomotors を外した URL へ 301 で転送する
// - /media/{ファイル名}  記事・4コマの画像と動画を R2（p2m-media）から返す（GitHub には入れないため）
// - /api/message         メッセージフォームの送信を R2（p2m-messages）に保存し、メールで知らせる
// - /api/hk-event        更新を知らせるハコブくんが「出た・押された・あとで見に来た」を R2（p2m-messages の hk-events/）に1件ずつ残す
// - /api/hk-stats        その記録を日ごと・更新ごとに数えて見せる（数だけ。誰かは分からない）
// - /api/ev              サイトの使われ方（1回のページ表示ごとの、読んだ深さ・押したもの・迷い）を R2（p2m-messages の ev/）に残す
// - /api/ev-stats        その記録を週ごと・ページごとに数えて見せる（数だけ。誰かは分からない）
// - ページ・llms.txt・全文の Markdown など  静的ファイルを返す。検索エンジン・AI のクローラーが読みに来たら、R2（p2m-messages の cr/）に1件ずつ残す
// - /api/cr-stats        その記録を、どのクローラーが・いつ・どのページを読んだかで数えて見せる（2026-10-25）
// - 5分ごとの scheduled  SNS の予約投稿の時刻が来ていたら GitHub Actions を起こす（worker/sns_cron.js）
//
// 守り（2026-10-23）
// - 数の表（/api/hk-stats・/api/ev-stats・/api/cr-stats）は、鍵（Cloudflare の秘密の値 STATS_KEY）を付けたときだけ見られる。鍵がなければ 404
// - 送信の口（/api/message・/api/hk-event・/api/ev）は、このサイトのページから送られたものだけ受け取る
// - 同じ送信元からの送りすぎは、Cloudflare の流量制限（wrangler.jsonc の ratelimits）で断る。送信元の IP アドレスは数えるためだけに使い、記録しない
// - フォームには人には見えない欄（website）を置き、そこが埋まった送信は保存せずに受け取ったふりをする（自動投稿の対策）
// - このファイルが返す応答には、内容の種類を推測させない指定（nosniff）を付ける。ページ側の指定は public/_headers
import { EmailMessage } from 'cloudflare:email';
import { snsCron } from './sns_cron.js';

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/media/') || url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) {
      return secured(await route(request, env, url));
    }
    const res = await env.ASSETS.fetch(request);
    // 検索エンジン・AI のクローラーが読みに来たか（記録の失敗でページを止めない）
    try {
      const bot = crawlerOf(request, url);
      if (bot) ctx.waitUntil(crRecord(request, env, url, bot, res.status).catch(() => {}));
    } catch { /* 数え損ねてもページは返す */ }
    return res;
  },
  // 5分ごと（wrangler.jsonc の triggers）：SNS の予約投稿の時刻が来ていたら、GitHub Actions を起こす（worker/sns_cron.js）
  async scheduled(event, env, ctx) {
    ctx.waitUntil(snsCron(env, event.scheduledTime).then((m) => console.log('sns-cron:', m), (e) => console.error('sns-cron:', e.message)));
  },
};

async function route(request, env, url) {
    // 旧 URL の形（/pixelstomotors/…）で来たら、/pixelstomotors を外した URL へ恒久転送する
    if (url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) {
      const to = new URL(url.pathname.slice('/pixelstomotors'.length) || '/', url);
      to.search = url.search;
      return Response.redirect(to.toString(), 301);
    }
    if (url.pathname.startsWith('/media/')) return media(request, env, url);
    if (url.pathname === '/api/message') return guarded(request, env, url, 'RL_MESSAGE', () => message(request, env));
    if (url.pathname === '/api/hk-event') return guarded(request, env, url, 'RL_EVENT', () => hkEvent(request, env));
    if (url.pathname === '/api/ev') return guarded(request, env, url, 'RL_EVENT', () => evRecord(request, env, url));
    if (url.pathname === '/api/hk-stats') return (await statsAllowed(request, env, url)) ? hkStats(request, env, url) : notFound();
    if (url.pathname === '/api/ev-stats') return (await statsAllowed(request, env, url)) ? evStats(request, env, url) : notFound();
    if (url.pathname === '/api/cr-stats') return (await statsAllowed(request, env, url)) ? crStats(request, env, url) : notFound();
    return env.ASSETS.fetch(request);
}

const notFound = () => new Response('Not Found', { status: 404, headers: { 'x-robots-tag': 'noindex' } });

// worker が返す応答に、内容の種類を推測させない指定などを付ける（転送の応答は見出しを変えられないので作り直す）
function secured(res) {
  const r = new Response(res.body, res);
  r.headers.set('x-content-type-options', 'nosniff');
  r.headers.set('referrer-policy', 'strict-origin-when-cross-origin');
  return r;
}

// このサイトのページから送られたものか（ブラウザは POST に Origin を付ける。付かないときは Sec-Fetch-Site で見る）
function sameSite(request, url) {
  const o = request.headers.get('origin');
  if (o) { try { return new URL(o).host === url.host; } catch { return false; } }
  return request.headers.get('sec-fetch-site') === 'same-origin';
}

// 送信の口の共通の守り：POST 以外・ほかのサイトから・送りすぎを断ってから処理する
async function guarded(request, env, url, limiter, handle) {
  if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
  if (!sameSite(request, url)) return new Response(null, { status: 403 });
  const rl = env[limiter];
  if (rl) {
    try {
      const { success } = await rl.limit({ key: request.headers.get('cf-connecting-ip') || 'unknown' });
      if (!success) return new Response(null, { status: 429, headers: { 'retry-after': '60' } });
    } catch { /* 流量制限が使えないときは通す（サイトの動きを止めない） */ }
  }
  return handle();
}

// 数の表を見てよいか。鍵は ?key=… か Authorization: Bearer … で渡す。鍵が設定されていなければ誰にも見せない
async function statsAllowed(request, env, url) {
  const key = env.STATS_KEY;
  if (!key) return false;
  const given = url.searchParams.get('key') || (request.headers.get('authorization') || '').replace(/^Bearer\s+/i, '');
  if (!given) return false;
  const [a, b] = await Promise.all([given, key].map((x) => crypto.subtle.digest('SHA-256', new TextEncoder().encode(x))));
  return crypto.subtle.timingSafeEqual(a, b);
}

async function media(request, env, url) {
  if (request.method !== 'GET' && request.method !== 'HEAD') return new Response('Method Not Allowed', { status: 405 });
  const key = decodeURIComponent(url.pathname.slice('/media/'.length));
  if (!key || key.includes('/') || key.startsWith('.')) return new Response('Not Found', { status: 404 });
  // 動画の途中から再生できるよう、Range（一部だけの取得）にも答える
  const obj = await env.MEDIA.get(key, { range: request.headers, onlyIf: request.headers });
  if (obj === null) return new Response('Not Found', { status: 404 });
  const headers = new Headers();
  obj.writeHttpMetadata(headers);
  headers.set('etag', obj.httpEtag);
  headers.set('accept-ranges', 'bytes');
  headers.set('cache-control', 'public, max-age=86400');
  if (!('body' in obj) || !obj.body) return new Response(null, { status: 304, headers });
  let status = 200;
  if (obj.range && request.headers.has('range')) {
    // R2 の range は {offset, length} か {suffix}。キーがあっても値が undefined のことがあるため、値で判定する
    // （'suffix' in r で判定すると NaN になり、ブラウザが動画を再生しなかった）
    const r = obj.range;
    let start, end;
    if (typeof r.suffix === 'number') {
      start = Math.max(0, obj.size - r.suffix);
      end = obj.size - 1;
    } else {
      start = typeof r.offset === 'number' ? r.offset : 0;
      end = typeof r.length === 'number' ? Math.min(obj.size - 1, start + r.length - 1) : obj.size - 1;
    }
    headers.set('content-range', `bytes ${start}-${end}/${obj.size}`);
    headers.set('content-length', String(end - start + 1));
    status = 206;
  }
  return new Response(request.method === 'HEAD' ? null : obj.body, { status, headers });
}

// メッセージフォームの「ご相談の種類」。値はフォーム（src/pages/message.astro）の option と同じ
const TOPICS = { 'physical-ai': 'フィジカルAI・ロボット', ai: 'AI活用・仕組みづくり', media: '取材・登壇', other: 'その他' };

async function message(request, env) {
  if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
  let d;
  try { d = await request.json(); } catch { return json({ ok: false, error: '送信の形が正しくありません' }, 400); }
  // 人には見えない欄が埋まっていたら自動投稿。保存も通知もせず、受け取ったふりをする
  if (String(d.website || '').trim()) return json({ ok: true });
  const name = String(d.name || '').trim().slice(0, 200);
  const email = String(d.email || '').trim().slice(0, 300);
  const body = String(d.body || '').trim().slice(0, 10000);
  // 相談の種類（フォームの選択肢と同じ値）。古いページから送られて種類がないときは「未選択」として受け取る
  const topic = Object.hasOwn(TOPICS, d.topic) ? d.topic : '';
  if (!name || !email || !body || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || d.agree !== true) {
    return json({ ok: false, error: '入力を確かめてください' }, 400);
  }
  const at = new Date().toISOString();
  const key = `${at.replace(/[:.]/g, '-')}-${crypto.randomUUID().slice(0, 8)}.json`;
  await env.MESSAGES.put(key, JSON.stringify({ at, name, email, topic, body, ua: request.headers.get('user-agent') || '' }, null, 1),
    { httpMetadata: { contentType: 'application/json' } });
  // 保存できたら、メールで知らせる。メールが送れなくても、保存は済んでいるので送信者には成功と返す
  try {
    await notify(env, { at, name, email, topic, body, key });
  } catch (e) {
    console.error('メール通知に失敗', e && e.message);
  }
  return json({ ok: true });
}

// 届いたメッセージを MAIL（Cloudflare の送信の仕組み）でエイイチのメールへ送る。
// 送り元はこのドメインの form@pixelstomotors.com。返信先（Reply-To）に送信者のメールを入れる
async function notify(env, m) {
  if (!env.MAIL) return;
  const from = 'form@pixelstomotors.com';
  const to = 'pixelstomotors@adaptdesign.jp';
  const topicName = Object.hasOwn(TOPICS, m.topic) ? TOPICS[m.topic] : '未選択';
  const subject = `[Pixels to Motors] ${topicName}: ${m.name}`;
  const text = [
    `種類: ${topicName}`,
    `名前: ${m.name}`,
    `メール: ${m.email}`,
    `受け取り: ${m.at}`,
    `保存先: p2m-messages/${m.key}`,
    '',
    m.body,
  ].join('\n');
  const raw = [
    `From: Pixels to Motors <${from}>`,
    `To: <${to}>`,
    `Reply-To: ${encodeWord(m.name)} <${m.email}>`,
    `Subject: ${encodeWord(subject)}`,
    `Date: ${new Date(m.at).toUTCString()}`,
    `Message-ID: <${m.key.replace(/\.json$/, '')}@pixelstomotors.com>`,
    'MIME-Version: 1.0',
    'Content-Type: text/plain; charset=utf-8',
    'Content-Transfer-Encoding: base64',
    '',
    base64(text),
  ].join('\r\n');
  await env.MAIL.send(new EmailMessage(from, to, raw));
}

// 日本語の見出しをメールの決まり（RFC 2047）で符号化する。見出しは1行に収めるので改行を入れない
function encodeWord(s) {
  return `=?utf-8?B?${base64(s, false)}?=`;
}

// fold=true のとき、本文用に 76 文字ごとに改行を入れる（見出しに入れると途中で切れて壊れる）
function base64(s, fold = true) {
  const bytes = new TextEncoder().encode(s);
  let bin = '';
  for (const b of bytes) bin += String.fromCharCode(b);
  const out = btoa(bin);
  return fold ? out.replace(/(.{76})/g, '$1\r\n') : out;
}

function json(o, status = 200) {
  return new Response(JSON.stringify(o), { status, headers: { 'content-type': 'application/json; charset=utf-8' } });
}

// ---------- 更新を知らせるハコブくんの効果の記録 ----------
// 1件 = 1つの空のファイル。中身は持たず、何が起きたかを付箋（customMetadata）に書く（一覧のときに中身を読まずに数えられる）
// 残すのは「出た(shown)／押された(click)／押さずにあとで見に来た(later)」「どの更新か」「どのページで」「画面の幅（pc/sp）」「日付」だけ。
// IP アドレス・ブラウザの種類・Cookie・見た人を見分ける番号は残さない
const HK_EVENTS = new Set(['shown', 'click', 'later']);
const HK_KINDS = new Set(['record', 'manga', 'issues']);
async function hkEvent(request, env) {
  if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
  let d;
  try { d = JSON.parse((await request.text()).slice(0, 2000)); } catch { return new Response(null, { status: 400 }); }
  const e = String(d.e || ''), kind = String(d.kind || '');
  const id = String(d.id || '').slice(0, 160), path = String(d.path || '').slice(0, 160), w = d.w === 'sp' ? 'sp' : 'pc';
  if (!HK_EVENTS.has(e) || !HK_KINDS.has(kind) || !/^(record|manga|issues):[\w.-]+$/.test(id) || !path.startsWith('/')) {
    return new Response(null, { status: 400 });
  }
  const at = new Date();
  const day = new Date(at.getTime() + 9 * 3600e3).toISOString().slice(0, 10); // 日本時間の日付
  const key = `hk-events/${day}/${at.getTime()}-${crypto.randomUUID().slice(0, 8)}`;
  await env.MESSAGES.put(key, '', { customMetadata: { e, kind, id, path, w } });
  return new Response(null, { status: 204 });
}

async function hkStats(request, env, url) {
  if (request.method !== 'GET') return new Response('Method Not Allowed', { status: 405 });
  const rows = [];
  let cursor;
  do {
    const r = await env.MESSAGES.list({ prefix: 'hk-events/', cursor, include: ['customMetadata'], limit: 1000 });
    for (const o of r.objects) rows.push({ day: o.key.split('/')[1], ...(o.customMetadata || {}) });
    cursor = r.truncated ? r.cursor : undefined;
  } while (cursor);
  const add = (map, k, ev) => { const v = map.get(k) || { shown: 0, click: 0, later: 0 }; v[ev] = (v[ev] || 0) + 1; map.set(k, v); return v; };
  // 週は月曜はじまり（日本時間の日付から、その週の月曜の日付を出す）
  const weekOf = (day) => { const d = new Date(day + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7)); return d.toISOString().slice(0, 10); };
  const byDay = new Map(), byWeek = new Map(), byItem = new Map(), byW = new Map();
  for (const r of rows) { add(byDay, r.day, r.e); add(byWeek, weekOf(r.day), r.e); add(byItem, r.id, r.e); add(byW, r.w, r.e); }
  const data = {
    total: rows.length,
    byWeek: [...byWeek].sort((a, b) => b[0].localeCompare(a[0])).map(([week, v]) => {
      const end = new Date(week + 'T00:00:00Z'); end.setUTCDate(end.getUTCDate() + 6);
      return { week: `${week} 〜 ${end.toISOString().slice(5, 10)}`, ...v };
    }),
    byDay: [...byDay].sort((a, b) => b[0].localeCompare(a[0])).map(([day, v]) => ({ day, ...v })),
    byItem: [...byItem].sort((a, b) => b[1].shown - a[1].shown).map(([id, v]) => ({ id, ...v })),
    byWidth: [...byW].map(([w, v]) => ({ w, ...v })),
  };
  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' } });
  }
  const pct = (v) => (v.shown ? `${Math.round(((v.click + v.later) / v.shown) * 100)}%` : '-');
  const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
  const table = (head, list, key) => `<table><tr><th>${head}</th><th>出た</th><th>吹き出しを押した</th><th>押さずにあとで見に来た</th><th>見に行った割合</th></tr>${
    list.map((v) => `<tr><td>${esc(v[key])}</td><td>${v.shown}</td><td>${v.click}</td><td>${v.later}</td><td>${pct(v)}</td></tr>`).join('')}</table>`;
  const html = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
<title>ハコブくんの知らせの記録</title><style>body{font:14px/1.7 -apple-system,'Noto Sans JP',sans-serif;margin:24px;color:#242424;background:#f7f7f7}
h1{font-size:20px}h2{font-size:16px;margin-top:28px}table{border-collapse:collapse;background:#fff;min-width:min(100%,640px)}th,td{border:1px solid #ddd;padding:6px 10px;text-align:left}th{background:#eee}p{color:#555}</style></head><body>
<h1>ハコブくんの知らせの記録</h1><p>サイトの更新を知らせてハコブくんが走った回数と、そのあと見た人がどう動いたか。数だけを残しており、誰かは分からない。日付は日本時間。</p>
<h2>週ごと（月曜はじまり。週が変わると新しい行が増える）</h2>${table('週', data.byWeek, 'week')}
<h2>日ごと</h2>${table('日付', data.byDay, 'day')}
<h2>更新ごと</h2>${table('更新', data.byItem, 'id')}
<h2>画面の幅（pc＝広い／sp＝スマホ）</h2>${table('幅', data.byWidth, 'w')}
<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' } });
}


// ---------- サイトの使われ方の記録（src/components/SiteMeasure.astro が送る） ----------
// 1回のページ表示 = 1件。中身は持たず、付箋（customMetadata）に書く（一覧のときに中身を読まずに数えられる）。
// 同じ表示が戻ってきて送り直したときは n が増える。数えるときは v（その表示だけの番号）ごとに n が最大の1件を使う。
// IP アドレス・ブラウザの種類・Cookie は残さない。v はブラウザに保存されない、その表示だけの番号
const EV_AREAS = new Set(['hakobu', 'header', 'footer', 'crumbs', 'toc', 'consult', 'pager', 'related', 'body', 'main', 'sim', 'other']);
const EV_DEPTHS = new Set([0, 25, 50, 75, 100]);
const evInt = (x, max) => { const n = Math.floor(Number(x)); return Number.isFinite(n) && n >= 0 ? Math.min(n, max) : 0; };
// 自動で巡回するプログラムの名乗り（検索エンジン・AI のクローラー・SNS のプレビュー・自動のブラウザ）。名乗りそのものは保存しない
const EV_BOT_UA = /bot\/|bot;|\bbot\b|crawl|spider|slurp|headless|lighthouse|preview|facebookexternalhit|embedly|python-|curl\/|wget|go-http|okhttp|axios|node-fetch|playwright|puppeteer|selenium|phantom/i;
// 人か機械か：b＝機械（自動のブラウザ・クローラーの名乗り）／h＝人の操作があった／x＝どちらとも言えない（操作なし）。2026-10-25 から
const evKind = (request, d) => {
  const ua = request.headers.get('user-agent') || '';
  let verified = false;
  try { verified = !!(request.cf && (request.cf.verifiedBotCategory || (request.cf.botManagement && request.cf.botManagement.verifiedBot))); } catch { /* 使えない環境では名乗りだけで見る */ }
  if (verified || !ua || EV_BOT_UA.test(ua) || Number(d.wd) === 1 || Number(d.hl) === 1) return 'b';
  return Number(d.u) === 1 ? 'h' : 'x';
};
const evText = (x, max) => String(x == null ? '' : x).replace(/[\u0000-\u001f|>]/g, ' ').slice(0, max);

async function evRecord(request, env, url) {
  if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
  // ほかのサイトから送られたもの・送りすぎは、guarded() で断っている
  let d;
  try { d = JSON.parse((await request.text()).slice(0, 6000)); } catch { return new Response(null, { status: 400 }); }
  const v = String(d.v || ''), p = String(d.p || '');
  if (!/^[a-z0-9]{6,16}$/.test(v) || !p.startsWith('/') || p.length > 160) return new Response(null, { status: 400 });
  const f = String(d.f || '');
  const s = Number(d.s);
  const clicks = (Array.isArray(d.c) ? d.c : []).slice(0, 8)
    .filter((c) => Array.isArray(c) && EV_AREAS.has(String(c[1])))
    .map((c) => `${evText(c[0], 100)}>${c[1]}`);
  let c = clicks.join('|');
  if (c.length > 900) c = c.slice(0, 900);
  const meta = {
    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() : '',
    w: d.w === 'sp' ? 'sp' : 'pc',
    s: String(EV_DEPTHS.has(s) ? s : 0),
    t: String(evInt(d.t, 7200)),
    c,
    d: String(evInt(d.d, 999)), g: String(evInt(d.g, 999)), cp: String(evInt(d.cp, 999)), vp: String(evInt(d.vp, 999)),
    fs: String(evInt(d.fs, 1)), fx: String(evInt(d.fx, 99)), fv: String(evInt(d.fv, 99)),
    k: evKind(request, d),
  };
  const at = new Date();
  const day = new Date(at.getTime() + 9 * 3600e3).toISOString().slice(0, 10); // 日本時間の日付
  await env.MESSAGES.put(`ev/${day}/${at.getTime()}-${v}-${meta.n}`, '', { customMetadata: meta });
  return new Response(null, { status: 204 });
}

// 週は月曜はじまり（日本時間の日付から、その週の月曜の日付を出す）
const evWeekOf = (day) => { const d = new Date(day + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7)); return d.toISOString().slice(0, 10); };
const evMedian = (a) => { if (!a.length) return 0; const b = [...a].sort((x, y) => x - y); const m = b.length >> 1; return b.length % 2 ? b[m] : Math.round((b[m - 1] + b[m]) / 2); };
const EV_AREA_JA = { hakobu: 'ハコブくん', header: 'ヘッダー', footer: 'フッター', crumbs: '現在地', toc: '目次', consult: '相談の案内', pager: '前後の記事', related: '関連記事', body: '本文', main: 'ページの中身', other: 'その他' };

async function evStats(request, env, url) {
  if (request.method !== 'GET') return new Response('Method Not Allowed', { status: 405 });
  const since = /^\d{4}-\d{2}-\d{2}$/.test(url.searchParams.get('since') || '') ? url.searchParams.get('since') : '';
  const week = /^\d{4}-\d{2}-\d{2}$/.test(url.searchParams.get('week') || '') ? url.searchParams.get('week') : '';
  // who=human で人だけ、who=bot で機械だけ、who=unknown で操作のなかった表示だけ、who=old で印を付ける前の記録だけを数える（なければ全部）
  const WHO = { human: 'h', bot: 'b', unknown: 'x', old: 'o' };
  const who = WHO[url.searchParams.get('who')] ? url.searchParams.get('who') : '';
  // 1) 記録を読む（v ごとに n が最大の1件）
  const views = new Map();
  let cursor, pages = 0, truncated = false;
  do {
    const r = await env.MESSAGES.list({ prefix: 'ev/', cursor, include: ['customMetadata'], limit: 1000, ...(since && !cursor ? { startAfter: `ev/${since}` } : {}) });
    for (const o of r.objects) {
      const m = o.customMetadata || {};
      if (!m.v) continue;
      const day = o.key.split('/')[1];
      if (since && day < since) continue;
      const prev = views.get(m.v);
      if (!prev || Number(m.n) >= Number(prev.n)) views.set(m.v, { ...m, day });
    }
    cursor = r.truncated ? r.cursor : undefined;
    if (++pages >= 40 && cursor) { truncated = true; break; }
  } while (cursor);
  // 2) メッセージフォームから実際に届いた数（p2m-messages の一番上に置かれた .json の数）
  const sent = new Map();
  cursor = undefined; pages = 0;
  do {
    const r = await env.MESSAGES.list({ delimiter: '/', cursor, limit: 1000 });
    for (const o of r.objects) {
      const m = o.key.match(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/);
      if (!m) continue;
      const day = new Date(Date.UTC(+m[1].slice(0, 4), +m[1].slice(5, 7) - 1, +m[1].slice(8, 10), +m[2], +m[3], +m[4]) + 9 * 3600e3).toISOString().slice(0, 10);
      if (since && day < since) continue;
      const w = evWeekOf(day); sent.set(w, (sent.get(w) || 0) + 1);
    }
    cursor = r.truncated ? r.cursor : undefined;
  } while (cursor && ++pages < 10);

  // 3) 数える
  const rows = [...views.values()].map((m) => ({
    ...m, week: evWeekOf(m.day), s: Number(m.s) || 0, t: Number(m.t) || 0,
    d: Number(m.d) || 0, g: Number(m.g) || 0, cp: Number(m.cp) || 0, vp: Number(m.vp) || 0,
    fs: Number(m.fs) || 0, fx: Number(m.fx) || 0, fv: Number(m.fv) || 0,
    k: m.k || 'o',
    clicks: m.c ? m.c.split('|').map((x) => { const i = x.lastIndexOf('>'); return { target: x.slice(0, i), area: x.slice(i + 1) }; }) : [],
  }));
  // 人と機械の内訳は、絞り込む前の全部の記録で週ごとに数える
  const kinds = new Map();
  for (const r of rows) { const x = kinds.get(r.week) || { h: 0, b: 0, x: 0, o: 0 }; x[r.k]++; kinds.set(r.week, x); }
  const counted = who ? rows.filter((r) => r.k === WHO[who]) : rows;
  const isMsg = (p) => p === '/message' || p === '/message/';
  const outside = (c) => c.area !== 'hakobu' && !c.target.startsWith('/') && !c.target.startsWith('#') && !c.target.startsWith('btn:') && !c.target.startsWith('(リンクなし)') && !c.target.startsWith('iframe');
  const weeks = new Map();
  for (const r of counted) {
    const w = weeks.get(r.week) || { week: r.week, views: 0, times: [], read75: 0, dead: 0, rage: 0, out: 0, back: 0, sp: 0, msgViews: 0, formStart: 0, formSubmit: 0, formValid: 0 };
    w.views++; w.times.push(r.t); if (r.s >= 75) w.read75++; w.dead += r.d; w.rage += r.g;
    w.out += r.clicks.filter(outside).length; if (r.f === '(戻る)') w.back++; if (r.w === 'sp') w.sp++;
    if (isMsg(r.p)) { w.msgViews++; w.formStart += r.fs; w.formSubmit += r.fx; w.formValid += r.fv; }
    weeks.set(r.week, w);
  }
  for (const k of sent.keys()) if (!weeks.has(k)) weeks.set(k, { week: k, views: 0, times: [], read75: 0, dead: 0, rage: 0, out: 0, back: 0, sp: 0, msgViews: 0, formStart: 0, formSubmit: 0, formValid: 0 });
  const pct = (a, b) => (b ? Math.round((a / b) * 100) : 0);
  const byWeek = [...weeks.values()].sort((a, b) => b.week.localeCompare(a.week)).map((w) => {
    const end = new Date(w.week + 'T00:00:00Z'); end.setUTCDate(end.getUTCDate() + 6);
    return {
      week: w.week, label: `${w.week} 〜 ${end.toISOString().slice(5, 10)}`, views: w.views, medianSeconds: evMedian(w.times),
      read75Pct: pct(w.read75, w.views), spPct: pct(w.sp, w.views), backViews: w.back, outClicks: w.out, deadClicks: w.dead, rageClicks: w.rage,
      messageViews: w.msgViews, formStarts: w.formStart, formSubmits: w.formSubmit, formSubmitsValid: w.formValid, messagesReceived: sent.get(w.week) || 0,
      humanViews: (kinds.get(w.week) || {}).h || 0, botViews: (kinds.get(w.week) || {}).b || 0, unknownViews: (kinds.get(w.week) || {}).x || 0, unmarkedViews: (kinds.get(w.week) || {}).o || 0,
    };
  });
  // ページごと・移り方・押したもの（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();
  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);
    if (r.s >= 25) pg.s25++; if (r.s >= 50) pg.s50++; if (r.s >= 75) pg.s75++; if (r.s >= 100) pg.s100++;
    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 ? '（サイト内）' : '（直接・不明）'));
    for (const c of r.clicks) {
      count(areas, c.area);
      if (c.target.startsWith('(リンクなし)')) count(dead, `${r.p} ${c.target}`);
      else count(targets, `${c.target}｜${EV_AREA_JA[c.area] || c.area}`);
    }
  }
  const top = (map, n, key) => [...map].sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => ({ [key]: k, count: v }));
  const data = {
    total: rows.length, counted: counted.length, truncated, since: since || null, week: week || null, who: who || null,
    byWeek,
    byPage: [...pagesMap.values()].sort((a, b) => b.views - a.views).slice(0, 100).map((p) => ({
      path: p.path, views: p.views, medianSeconds: evMedian(p.times),
      read25Pct: pct(p.s25, p.views), read50Pct: pct(p.s50, p.views), read75Pct: pct(p.s75, p.views), read100Pct: pct(p.s100, p.views),
      deadClicks: p.dead, rageClicks: p.rage, copies: p.copies, videoPlays: p.plays, outClicks: p.out,
    })),
    moves: top(moves, 50, 'move'),
    referrers: top(refs, 30, 'from'),
    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'),
  };
  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' } });
  }
  const esc = (s) => String(s).replace(/[&<>"]/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ch]));
  const table = (cols, list) => list.length
    ? `<div class="wrap"><table><tr>${cols.map(([, h]) => `<th>${h}</th>`).join('')}</tr>${list.map((r) => `<tr>${cols.map(([k]) => `<td>${esc(r[k])}</td>`).join('')}</tr>`).join('')}</table></div>`
    : '<p>まだ記録がありません。</p>';
  const html = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
<title>サイトの使われ方の記録</title><style>body{font:14px/1.7 -apple-system,'Noto Sans JP',sans-serif;margin:24px;color:#242424;background:#f7f7f7}
h1{font-size:20px}h2{font-size:16px;margin-top:28px}.wrap{overflow-x:auto}table{border-collapse:collapse;background:#fff}th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;white-space:nowrap}th{background:#eee}p{color:#555}</style></head><body>
<h1>サイトの使われ方の記録</h1><p>1回のページ表示ごとに、どこまで読んだか・何を押したか・迷ったかを数えたもの。誰かは分からない。日付は日本時間、週は月曜はじまり。${week ? `ページごと以下の表は ${esc(week)} の週だけ。` : 'ページごと以下の表は全期間。?week=週の月曜の日付 でその週だけになる。'}${truncated ? ' 記録が多すぎて一部だけ数えた（?since=日付 で期間を絞る）。' : ''}</p>
<p>人と機械：「人」はマウス・指・キーの操作があった表示、「機械」はクローラーや自動のブラウザの名乗り・印があった表示、「操作なし」はどちらとも言えない表示、「印なし」は 2026-10-25 より前の記録。いま数えているのは <b>${esc({ human: '人', bot: '機械', unknown: '操作なし', old: '印なし' }[who] || '全部')}</b>（?who=human・bot・unknown・old で絞る。週ごとの表の「人」〜「印なし」の4列は常に全部の内訳）。</p>
<h2>週ごと</h2>${table([['label', '週'], ['views', '表示'], ['medianSeconds', '表示時間（中央値・秒）'], ['read75Pct', '75％以上読んだ（％）'], ['spPct', 'スマホ（％）'], ['backViews', '戻るで戻った'], ['outClicks', '外へのクリック'], ['deadClicks', 'リンクなしを押した'], ['rageClicks', '連打'], ['messageViews', 'フォームの表示'], ['formStarts', '入力を始めた'], ['formSubmits', '送信を押した'], ['formSubmitsValid', 'うち入力がそろっていた'], ['messagesReceived', '届いたメッセージ'], ['humanViews', '人'], ['botViews', '機械'], ['unknownViews', '操作なし'], ['unmarkedViews', '印なし']], byWeek)}
<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>押したリンク・ボタン（行き先｜ページのどの部分か）</h2>${table([['target', '押したもの'], ['count', '回数']], data.clicks)}
<h2>押した場所（ページのどの部分か）</h2>${table([['area', '部分'], ['count', '回数']], data.areas)}
<h2>リンクでない所を押した（ページと、押したものの種類）</h2>${table([['where', '場所'], ['count', '回数']], data.deadClicks)}
<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' } });
}

// ───────── 検索エンジン・AI のクローラーの訪問（2026-10-25）─────────
// ページ上の計測（/api/ev）は JavaScript を動かさないクローラーを数えられないので、配信のときに名乗りで数える。
// 残すのは「どのクローラーか・どのページか・返した結果（200 など）・Cloudflare が本物と確かめたか・日時」だけ。名乗りそのものは保存しない。
// 名乗りはまねできるので、「確かめた」が 0 のものは偽物が混ざりうる（Cloudflare が判定を渡さない環境では ? になる）
const CRAWLERS = [
  ['GPTBot', 'OpenAI（学習）'], ['OAI-SearchBot', 'OpenAI（ChatGPT の検索）'], ['ChatGPT-User', 'ChatGPT（利用者の依頼で読む）'],
  ['ClaudeBot', 'Anthropic（学習）'], ['Claude-SearchBot', 'Anthropic（Claude の検索）'], ['Claude-User', 'Claude（利用者の依頼で読む）'], ['anthropic-ai', 'Anthropic'],
  ['PerplexityBot', 'Perplexity（検索）'], ['Perplexity-User', 'Perplexity（利用者の依頼で読む）'],
  ['Google-CloudVertexBot', 'Google（Vertex AI）'], ['GoogleOther', 'Google（その他）'], ['Googlebot', 'Google（検索）'],
  ['bingbot', 'Bing（検索。ChatGPT の検索・Copilot も使う）'], ['DuckAssistBot', 'DuckDuckGo（AI の回答）'], ['DuckDuckBot', 'DuckDuckGo（検索）'],
  ['Applebot', 'Apple（Siri・検索・AI）'], ['Amazonbot', 'Amazon（Alexa・AI）'], ['meta-externalagent', 'Meta（AI）'], ['FacebookBot', 'Meta'],
  ['Bytespider', 'ByteDance（AI）'], ['CCBot', 'Common Crawl（多くの AI の学習データ）'], ['MistralAI-User', 'Mistral（利用者の依頼で読む）'],
  ['cohere-ai', 'Cohere'], ['Diffbot', 'Diffbot'], ['YouBot', 'You.com'], ['YandexBot', 'Yandex（検索）'], ['Baiduspider', 'Baidu（検索）'], ['Yeti', 'Naver（検索）'],
];
// 数えるのはページ・テキストだけ（画像・CSS・JS は wrangler.jsonc の run_worker_first で外している）
const CR_PAGE = /(\/|\/[^/.]*|\.html|\.md|\.txt|\.xml|\.json)$/i;
function crawlerOf(request, url) {
  if (request.method !== 'GET' && request.method !== 'HEAD') return null;
  if (!CR_PAGE.test(url.pathname)) return null;
  const ua = (request.headers.get('user-agent') || '').toLowerCase();
  if (!ua) return null;
  for (const [name] of CRAWLERS) if (ua.includes(name.toLowerCase())) return name;
  return null;
}
async function crRecord(request, env, url, bot, status) {
  let ok = '?';
  try {
    const cf = request.cf || {};
    if ('verifiedBotCategory' in cf) ok = cf.verifiedBotCategory ? '1' : '0';
    else if (cf.botManagement && 'verifiedBot' in cf.botManagement) ok = cf.botManagement.verifiedBot ? '1' : '0';
  } catch { /* 判定がなければ ? のまま */ }
  const at = new Date();
  const day = new Date(at.getTime() + 9 * 3600e3).toISOString().slice(0, 10);
  const meta = { b: bot, p: url.pathname.slice(0, 160), s: String(status), ok };
  await env.MESSAGES.put(`cr/${day}/${at.getTime()}-${Math.random().toString(36).slice(2, 8)}`, '', { customMetadata: meta });
}
async function crStats(request, env, url) {
  if (request.method !== 'GET') return new Response('Method Not Allowed', { status: 405 });
  const since = /^\d{4}-\d{2}-\d{2}$/.test(url.searchParams.get('since') || '') ? url.searchParams.get('since') : '';
  const only = url.searchParams.get('bot') || '';
  const rows = [];
  let cursor, pages = 0, truncated = false;
  do {
    const r = await env.MESSAGES.list({ prefix: 'cr/', cursor, include: ['customMetadata'], limit: 1000, ...(since && !cursor ? { startAfter: `cr/${since}` } : {}) });
    for (const o of r.objects) {
      const m = o.customMetadata || {};
      if (!m.b) continue;
      const day = o.key.split('/')[1];
      if (since && day < since) continue;
      rows.push({ ...m, day, week: evWeekOf(day) });
    }
    cursor = r.truncated ? r.cursor : undefined;
    if (++pages >= 40 && cursor) { truncated = true; break; }
  } while (cursor);
  const label = Object.fromEntries(CRAWLERS);
  const count = (map, k) => map.set(k, (map.get(k) || 0) + 1);
  // クローラーごと（全期間）
  const bots = new Map();
  for (const r of rows) {
    const b = bots.get(r.b) || { bot: r.b, who: label[r.b] || r.b, visits: 0, pages: new Set(), records: new Set(), verified: 0, unverified: 0, errors: 0, last: '' };
    b.visits++; b.pages.add(r.p); if (r.p.startsWith('/record/') || r.p.startsWith('/files/')) b.records.add(r.p.split('/').slice(0, 3).join('/'));
    if (r.ok === '1') b.verified++; else if (r.ok === '0') b.unverified++;
    if (Number(r.s) >= 400) b.errors++;
    if (r.day > b.last) b.last = r.day;
    bots.set(r.b, b);
  }
  // 週ごと × クローラー
  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= でそのクローラーだけ）
  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); }
  const data = {
    total: rows.length, truncated, since: since || null, bot: only || null,
    byBot: [...bots.values()].sort((a, b) => b.visits - a.visits).map((b) => ({
      bot: b.bot, who: b.who, visits: b.visits, pages: b.pages.size, records: b.records.size, verified: b.verified, unverified: b.unverified, errors: b.errors, last: b.last,
    })),
    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('、') })),
  };
  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' } });
  }
  const esc = (s) => String(s).replace(/[&<>"]/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ch]));
  const table = (cols, list) => list.length
    ? `<div class="wrap"><table><tr>${cols.map(([, h]) => `<th>${h}</th>`).join('')}</tr>${list.map((r) => `<tr>${cols.map(([k]) => `<td>${esc(r[k])}</td>`).join('')}</tr>`).join('')}</table></div>`
    : '<p>まだ記録がありません。</p>';
  const html = `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
<title>クローラーの訪問の記録</title><style>body{font:14px/1.7 -apple-system,'Noto Sans JP',sans-serif;margin:24px;color:#242424;background:#f7f7f7}
h1{font-size:20px}h2{font-size:16px;margin-top:28px}.wrap{overflow-x:auto}table{border-collapse:collapse;background:#fff}th,td{border:1px solid #ddd;padding:6px 10px;text-align:left;white-space:nowrap}th{background:#eee}p{color:#555}</style></head><body>
<h1>クローラーの訪問の記録</h1><p>検索エンジン・AI のクローラーが、どのページを読みに来たか（2026-10-25 から）。名乗りで見分けているので、「Cloudflare が確かめた」が 0 のものは偽物が混ざりうる。日付は日本時間、週は月曜はじまり。?since=日付 で期間を、?bot=名前（例 GPTBot）でページごとの表をそのクローラーだけに絞る。${truncated ? ' 記録が多すぎて一部だけ数えた。' : ''}</p>
<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)}
<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' } });
}
```

### コード：配信の設定 wrangler.jsonc

```jsonc
{
  // Cloudflare Workers の設定。
  // ページは作っておいた静的ファイル（dist/）。画像と動画・メッセージフォームだけ worker/index.js が受け持つ
  "name": "p2m-site",
  "main": "./worker/index.js",
  "compatibility_date": "2026-10-01",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",
    "not_found_handling": "404-page",
    "html_handling": "auto-trailing-slash",
    // ページ・テキストも worker/index.js を通す（検索エンジン・AI のクローラーの訪問を数えるため。2026-10-25）。
    // 画像・CSS・JS・図解・試算の部品は数えないので、静的ファイルから直接返す（! が付いたもの）
    "run_worker_first": ["/*", "!/_astro/*", "!/site/*", "!/og/*", "!/thumbs/*", "!/embeds/*", "!/sim/*", "!/favicon*"]
  },
  "r2_buckets": [
    // 記事・4コマの画像と動画（GitHub には入れない。tools/upload_media.py で Mac から入れる）
    { "binding": "MEDIA", "bucket_name": "p2m-media" },
    // メッセージフォームで送られた内容
    { "binding": "MESSAGES", "bucket_name": "p2m-messages" }
  ],
  // ログ（エラーの記録）。管理画面でオンにしても、公開のたびにこのファイルの内容で上書きされるので、ここに書く（2026-10-23）
  "observability": {
    "logs": { "enabled": true, "head_sampling_rate": 1, "invocation_logs": true, "persist": true }
  },
  // 5分ごとに worker/index.js の scheduled を動かす（SNS の予約投稿を時刻どおりに起こす。worker/sns_cron.js）
  "triggers": { "crons": ["*/5 * * * *"] },
  // 送信の口の流量制限（同じ送信元から60秒あたりの回数）。worker/index.js の guarded() が使う。
  // namespace_id はこのアカウントの中で他と重ならない番号なら何でもよい
  "ratelimits": [
    { "name": "RL_MESSAGE", "namespace_id": "2310231", "simple": { "limit": 5, "period": 60 } },
    { "name": "RL_EVENT", "namespace_id": "2310232", "simple": { "limit": 60, "period": 60 } }
  ],
  // メッセージフォームに届いた内容をメールで知らせる（Cloudflare の Email Routing で確認済みの宛先にだけ送れる）
  "send_email": [
    { "name": "MAIL", "destination_address": "pixelstomotors@adaptdesign.jp" }
  ]
}
```

### コード：AI 向けの案内を作る src/pages/llms.txt.ts

```ts
// /llms.txt：AI がこのサイトを読むときの案内（2026-10-25）。https://llmstxt.org の形に合わせる。
// 主要な AI が公式に使うとは言っていないので効果ははっきりしないが、作るのは軽いので置く。
// ビルドのたびに、シリーズ（src/lib/records.ts の SERIES_PAGES）と記録の一覧から作り直す。
// 長いコード・ログを外に分けた記事は、全文の Markdown（public/files/LOG{N}/index.json の article_full_markdown）も添える
import fs from 'node:fs';
import path from 'node:path';
import { allCards, SERIES_PAGES } from '../lib/records';
import { SITE } from '../lib/seo';

const fullMarkdown = (day: string): string | null => {
  try {
    const j = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'public/files', day, 'index.json'), 'utf8'));
    const u = j.article_full_markdown || (j.article_full_type === 'text/markdown' ? j.article_full : null);
    return u ? (u.startsWith('http') ? u : SITE + u) : null;
  } catch { return null; }
};

export async function GET() {
  const cards = await allCards();
  const seriesName = (s: string) => Object.values(SERIES_PAGES).find((c) => c.series === s)?.title.split(' | ').pop() || s;
  const lines: string[] = [
    '# Pixels to Motors',
    '',
    '> ADAPT DESIGN（島津 栄一 / Eiichi Shimazu）による、フィジカルAI・ロボットの研究記録のサイト。机の上のロボットアームでお手本のデータを集め、AI に学習させ、実機で動かして確かめる環境を自宅で組み、誰でも同じ手順で始められる形で公開している。サービスロボット（配膳・搬送・清掃など）の導入前シミュレーションも行っている。',
    '',
    '- 記録（LOG）は作業の過程をそのまま残したもので、コード・実行ログ・数値を全量で載せている。長いコードとログは別ファイルに分け、記事の全文の Markdown を添えている',
    '- 試算の前提の数字は、公開事例と観察から置いた仮の値。記録が新しいほど前提が更新されている（公開済みの記録は直さない）',
    '- 言語は日本語',
    '',
    '## シリーズ',
    '',
    ...Object.entries(SERIES_PAGES).map(([slug, c]) => {
      const [en, ja] = c.title.split(' | ');
      return `- [${ja || c.title}（${en}）](${SITE}/${slug})${c.intro ? `: ${c.intro}` : ''}`;
    }),
    '',
    '## 導入前ガイドと考え',
    '',
    `- [配膳ロボットの導入前シミュレーション](${SITE}/serving-robot-one-less-staff): 店舗の図面と人数から、何人で回るか・どこで止まるか・月いくらかかるかを出す`,
    `- [ロボットAIの学習用GPU、買うか借りるか](${SITE}/gpu-buy-or-rent): 実費と損益分岐の計算機で決める導入ガイド`,
    `- [ロボットに、何を任せればいいのか](${SITE}/thinking): 考察記事から、いま考えていること`,
    `- [Pixels to Motors の1年](${SITE}/one-year): 画面の中の AI から、ロボットを動かす AI へ`,
    `- [課題地図](${SITE}/issues): フィジカルAIが社会実装される前に解くべき課題（週1回更新）`,
    '',
    '## 記録（新しい順）',
    '',
    ...cards.map((c) => {
      const md = fullMarkdown(c.day);
      return `- [${c.day} ${c.title}](${SITE}/record/${c.slug}): ${seriesName(c.series)}${md ? `（全文の Markdown: ${md}）` : ''}`;
    }),
    '',
    '## Optional',
    '',
    `- [4コママンガ](${SITE}/manga-archive): 薬剤搬送ロボット「ハコブくん」の4コマ`,
    `- [メッセージ](${SITE}/message): 問い合わせフォーム`,
    '',
  ];
  return new Response(lines.join('\n'), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
```

### コード：Bing の所有者確認 public/BingSiteAuth.xml

```xml
<?xml version="1.0"?>
<users>
	<user>3AB7818E9A5C06DB0AE2E883B8555FDB</user>
</users>
```

### コード：人と機械の判定を名乗りで確かめたコマンド

```bash
node -e '
const src=require("fs").readFileSync("worker/index.js","utf8");
const a=src.indexOf("const EV_BOT_UA"), b=src.indexOf("const evText");
eval(src.slice(a,b).replace(/const /g,"var "));
const req=(ua,cf)=>({headers:{get:()=>ua},cf});
const C=[["Mozilla/5.0 (Macintosh) Chrome/130 Safari/537.36",{u:1}],["Mozilla/5.0 (Macintosh) Chrome/130 Safari/537.36",{u:0}],["Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",{u:0}],["Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0)",{u:1}],["Mozilla/5.0 HeadlessChrome/130",{u:1}],["Mozilla/5.0 (Linux; Android 12; Cubot X50) Chrome/130 Mobile",{u:1}],["Mozilla/5.0 Chrome/130",{u:1,wd:1}],["",{u:1}]];
for(const [ua,d] of C) console.log(evKind(req(ua),d), ua.slice(0,60), JSON.stringify(d));'
```

### コード：ページから送る中身を確かめたスクリプト（Playwright）

```js
const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch();
  for (const human of [false, true]) {
    const ctx = await b.newContext(human ? { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Safari/537.36' } : {});
    const p = await ctx.newPage();
    await p.addInitScript(() => { navigator.sendBeacon = (u, blob) => { blob.text().then((t) => console.log('EV ' + t)); return true; }; });
    p.on('console', (m) => { const s = m.text(); if (s.startsWith('EV ')) { const d = JSON.parse(s.slice(3)); console.log(human ? 'mouse' : 'plain', JSON.stringify({ p: d.p, u: d.u, wd: d.wd, hl: d.hl })); } });
    await p.goto('http://localhost:4321/series8', { waitUntil: 'load' });
    if (human) { await p.mouse.move(100, 200); await p.mouse.move(150, 260); }
    await p.evaluate(() => { Object.defineProperty(document, 'hidden', { value: true, configurable: true }); document.dispatchEvent(new Event('visibilitychange')); });
    await p.waitForTimeout(800);
    await ctx.close();
  }
  await b.close();
})();
```

### コード：公開した series8 と llms.txt の画面を撮ったスクリプト（Playwright）

```js
const { chromium } = require('playwright');
(async () => {
  const b = await chromium.launch();
  const p = await b.newPage({ viewport: { width: 1400, height: 900 }, deviceScaleFactor: 2 });
  await p.goto('https://pixelstomotors.com/?p2m-notrack=1', { waitUntil: 'load' });
  await p.waitForTimeout(500);
  await p.addStyleTag({ content: '.hk-parade{display:none!important}' }).catch(()=>{});
  await p.goto('https://pixelstomotors.com/series8', { waitUntil: 'networkidle' });
  await p.addStyleTag({ content: '.hk-parade,[class*="hk-"]{display:none!important}' });
  await p.waitForTimeout(1200);
  await p.screenshot({ path: '/mnt/user-data/outputs/LOG385_images/LOG385_13_series8_after.png' });
  await p.goto('https://pixelstomotors.com/llms.txt', { waitUntil: 'load' });
  await p.screenshot({ path: '/mnt/user-data/outputs/LOG385_images/LOG385_14_llms_txt.png' });
  await b.close();
})();
```

### コード：公開後にページ・llms.txt・クローラーの記録を確かめたコマンド

```bash
for u in / /series8 /record/serving-robot-map-planner /llms.txt /files/LOG382/LOG382_full.md /site/pixelstomotors-logo.svg /robots.txt /pixelstomotors/series8; do curl -s -o /dev/null -w "%{http_code} $u\n" "https://pixelstomotors.com$u"; done; curl -s https://pixelstomotors.com/llms.txt | head -2; curl -s -o /dev/null -w "spoof %{http_code}\n" -A "Mozilla/5.0 (compatible; Yeti/1.1; +https://naver.me/spd)" "https://pixelstomotors.com/message"

# Mac の上（集計の鍵は KIOXIA から読む）
K=$(tr -d '\n\r ' < $HOME/mnt/KIOXIA/p2m_stats_key.txt); curl -s -H "Authorization: Bearer $K" "https://pixelstomotors.com/api/cr-stats?format=json" | python3 -c "
import json,sys;d=json.load(sys.stdin);print('total',d['total']);[print(b) for b in d['byBot']];print(d['byPage'][:8])"
```
