記録が検索で1位に出た ― 次はAIの回答の出典へ

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 に拾われているかを数えられるようにする

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(9本)

01_sec01.txt ― 出力:週ごと・ページごと・どこから来たか・押したもの(上位25件ずつ)・11,058字

02_sec01.txt ― 差分:全ページの見出しを日本語先にした(commit 44586e3)・6,793字

03_sec01.txt ― 差分:トップの解説動画の見出し public/embeds/top-video.html(commit c80d7b1)・5,232字

04_sec01.txt ― 差分:シリーズのページの見出しと series8 の説明文(commit 62b9110)・6,554字

05_sec01.txt ― 差分:人と機械の印(commit 7b50e3e)・13,189字

06_sec01.txt ― コード:シリーズのページ src/pages/[series].astro・13,453字

07_sec01.txt ― コード:シリーズの設定 src/lib/records.ts・7,337字

08_sec01.txt ― コード:サイトの使われ方の記録 src/components/SiteMeasure.astro・10,475字

09_sec01.js.txt ― コード:配信とクローラーの記録・集計 worker/index.js・35,578字

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

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

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

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

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

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

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

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

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

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

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

シークレットウィンドウで「サービスロボット 導入 シミュレーション」を検索した画面。AI による概要の下、通常の検索結果の1番目が pixelstomotors.com の series8
同じ検索結果を下へ送った画面。1位の series8 の下に、ソフトバンクロボティクス・Gravio・富士ソフトのページが続く

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

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

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

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

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

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つの現場の試算
直したあとの 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 も含めて探し直しています。

直す前のトップの解説動画の段。見出しが「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回)が入口として一番働いていて、古い記録(LOG18・17・126・19 など)も今も読まれていました。LOG18 は表示時間の中央値が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・図解・試算の部品
静的ファイルを直接返す
静的ファイルを直接返す(数えない)

人がマウスを動かして読むと閲覧の記録に「人」の印が残り、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本でした。

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

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

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

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

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

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

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

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

所有者の確認の方法を選ぶ画面。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 )を送り、いまは「処理中」です。

確認が通ったあとの Bing Webmaster Tools のホーム。左上が pixelstomotors.com/ になり、データは最大48時間で反映されると出ている
サイトマップを送った画面。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 から読み、画面に出していない)

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件ずつ)

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])
01_sec01.txt11,058字・133行・text
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}
別のタブで開く保存

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

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)

02_sec01.txt6,793字・133行・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>
別のタブで開く保存

差分:トップの解説動画の見出し public/embeds/top-video.html(commit c80d7b1)

03_sec01.txt5,232字・15行・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 @@
 
別のタブで開く保存

差分:シリーズのページの見出しと series8 の説明文(commit 62b9110)

04_sec01.txt6,554字・91行・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
 
別のタブで開く保存

差分:人と機械の印(commit 7b50e3e)

05_sec01.txt13,189字・143行・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 @@
 // 送るもの:ページ・サイト内のどのページから来たか(戻るボタンで戻ったか)・外から来たならそのサイトの名前(ドメイン)だけ・
別のタブで開く保存

差分:README の説明(commit 5a5adaf)

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)

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

06_sec01.txt13,453字・206行・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';
別のタブで開く保存

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

07_sec01.txt7,337字・77行・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 };
別のタブで開く保存

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

08_sec01.txt10,475字・218行・astro
---
// サイトの使われ方の記録(2026-10-18)。Web改善の検証(Project の claude/web_improvement_trial.md)のため、
// Cloudflare Web Analytics では取れない「どこまで読んだか・何を押したか・どこで迷ったか」を数える。
// 最初は広く取り、データがたまってから見るものを絞る方針。
//
// 1回のページ表示 = 1件。ページが見えなくなったとき(閉じる・移る・タブを切り替える)に、まとめて /api/ev へ送る。
// 戻ってきてまた見えなくなったら、同じ番号で中身を足して送り直す(数えるときは番号ごとに最後の1件だけ使う)。
//
別のタブで開く保存

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

09_sec01.js.txt35,578字・561行・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/)に残す
別のタブで開く保存

コード:配信の設定 wrangler.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

// /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 version="1.0"?>
<users>
	<user>3AB7818E9A5C06DB0AE2E883B8555FDB</user>
</users>

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

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)

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)

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・クローラーの記録を確かめたコマンド

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])"