commit daae7c1
アクセスの数から見た小さな改善4つ（計測の直し・導入比較ガイドの結論・古い記録から今への道・記事の素材の入口）

- ハコブくんの記録も ?p2m-notrack=1 と「トラッキングしないで」に従い、機械の印（k）を付ける。/api/hk-stats に人と機械の表と ?who=notbot・bot・old
- 導入比較ガイドの最初の画面の文を、結論（月180時間より少ないなら借りる方が安い・買うならGPUメモリ24GB以上）に変える
- 公開から60日より前の記録のタイトルの下に、シリーズの最新（なければサイト全体の最新）への道を出す（数える場所 now）
- 分けたファイルのある記事のタイトルの下に、素材の枠（#files）への入口を出す（数える場所 files）
- README に、Claude の確認では ?p2m-notrack=1 を付けることと、上の決まりを書く

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LevEXSYG9NkMRzRGxEeaqX


diff --git a/README.md b/README.md
index f85c664..1467f37 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,9 @@ Web改善の検証（Project の `claude/web_improvement_trial.md`）のため
 - エイイチや 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回の表示だけのもので、ブラウザに保存しない
+- Claude が公開後に自動のブラウザで確かめるときは、最初に開くページに `?p2m-notrack=1` を付ける（そのブラウザでは以後送らない）。付け忘れても navigator.webdriver で「機械」に入るので、人の数には混ざらない（2026-10-28）
+- ハコブくんの記録（/api/hk-event）も、`?p2m-notrack=1` の設定と「トラッキングしないで」の設定に従う。機械の印（k：b＝機械／n＝機械の印なし。2026-10-28 より前は印なし）を付け、`/api/hk-stats?who=notbot`・`bot`・`old` で絞れる（2026-10-28）
+- 記事ページの入口（2026-10-28）：公開から60日より前の記録は、タイトルの下に「今の続き」（「{シリーズ名}の最新」＝同じシリーズの60日以内の最新。なければ「いま進めている記録」＝サイト全体の最新）を出す（押すと `今の続き>now` で数える）。分けたファイルのある記事は、タイトルの下に「この記事の素材」（本文の「ファイル一式」の枠 #files へ飛ぶ。`素材>files`）を出す
 - 押したものの「ページのどの部分か」は、クラス名（.richText・.related・.consult など）で決めている。部品に `data-ev-area="名前"` を付けると、その名前で数える（名前は `worker/index.js` の `EV_AREAS` にあるものだけ受け付ける）。リンクやボタンに `data-ev="名前"` を付けると、行き先の前にその名前が付く
 
 記事・4コマの画像と動画は GitHub に入れない。ただし `public/og/` の画像は、GitHub にあるサムネイルの HTML から書き出したものなので GitHub に入れる。サイトの `/media/ファイル名` を `worker/index.js` が R2 から返す。
diff --git a/src/components/HakobuParade.astro b/src/components/HakobuParade.astro
index c9de857..af81f24 100644
--- a/src/components/HakobuParade.astro
+++ b/src/components/HakobuParade.astro
@@ -73,8 +73,21 @@
   }
 
   // 効果の記録を送る（ページを移っても届くよう sendBeacon を使う）
+  // 送らない場合（サイトの使われ方の記録と同じ決まり。2026-10-28）：?p2m-notrack=1 で記録しない設定にしたブラウザ（エイイチ・Claude の確認）、
+  // 「トラッキングしないで」の設定のブラウザ
+  // 機械の印（2026-10-28）：wd＝navigator.webdriver、hl＝名乗りに Headless・bot などが入っているかの判定結果だけ（名乗りそのものは送らない）
+  const HK_AUTO_UA = /headless|lighthouse|bot\/|bot;|\bbot\b|crawl|spider|slurp|preview|phantom|puppeteer|playwright|selenium/i;
+  function reportOff(): boolean {
+    const nav = navigator as Navigator & { globalPrivacyControl?: boolean };
+    if (nav.doNotTrack === '1' || nav.globalPrivacyControl === true) return true;
+    try { return localStorage.getItem('p2m-notrack') === '1' || new URLSearchParams(location.search).get('p2m-notrack') === '1'; } catch { return false; }
+  }
   function report(e: 'shown' | 'click' | 'later', it: { id: string; kind: string }) {
-    const body = JSON.stringify({ e, id: it.id, kind: it.kind, path: location.pathname, w: window.innerWidth < 600 ? 'sp' : 'pc' });
+    if (reportOff()) return;
+    const body = JSON.stringify({
+      e, id: it.id, kind: it.kind, path: location.pathname, w: window.innerWidth < 600 ? 'sp' : 'pc',
+      wd: navigator.webdriver ? 1 : 0, hl: HK_AUTO_UA.test(navigator.userAgent || '') ? 1 : 0,
+    });
     try {
       if (!navigator.sendBeacon || !navigator.sendBeacon('/api/hk-event', new Blob([body], { type: 'text/plain' }))) {
         fetch('/api/hk-event', { method: 'POST', body, keepalive: true }).catch(() => {});
diff --git a/src/pages/gpu-buy-or-rent.astro b/src/pages/gpu-buy-or-rent.astro
index ccb2906..c3b9fcd 100644
--- a/src/pages/gpu-buy-or-rent.astro
+++ b/src/pages/gpu-buy-or-rent.astro
@@ -43,7 +43,7 @@ const jsonLd = [
     <div class="fv-text">
       <p class="who">これからロボットAIの学習環境を用意する、<br>企業の担当者・研究者・エンジニア向け</p>
       <h1 id="fv-h">ロボット<span class="lat">AI</span>の<br>学習用<span class="lat">GPU</span>、<br>買うか借りるか</h1>
-      <p class="fv-lead">買う前に借りて、実際に学習させました。見積書と、利用明細を並べます。実費と計算機で、自分の場合どちらが合うかを決められる導入ガイドです。</p>
+      <p class="fv-lead"><b>結論：使うのが月180時間より少ないなら、借りる方が安い。</b>RTX 5090のPCを4年使う場合の目安です。買うなら、GPUのメモリは24GB以上が条件です。実際に借りて学習させた実費（7回で4,677円）と、自分の条件で確かめられる計算機を置いています。</p>
       <div class="fv-cta"><a class="btn dark" href="#calc">自分の条件で計算する<i class="ms" aria-hidden="true">arrow_forward</i></a></div>
     </div>
     <div class="fv-docs">
@@ -724,6 +724,7 @@ const jsonLd = [
 /* 英字（Manrope の一番太い800）を、日本語の900と同じ太さに見せる */
 .gbr .fv h1 .lat,.gbr h2 .lat,.gbr .h-num{-webkit-text-stroke:.028em currentColor;paint-order:stroke fill;letter-spacing:.01em}
 .gbr .fv-lead{margin:0;font-size:1rem;line-height:1.85;color:#2a2d33;max-width:30em}
+.gbr .fv-lead b{display:block;margin-bottom:6px;font-size:1.15rem;line-height:1.6;color:var(--ink)}
 .gbr .fv-cta{display:flex;flex-wrap:wrap;gap:10px}
 .gbr .fv-docs{display:grid;grid-template-columns:minmax(0,380px) 120px minmax(0,380px);align-items:stretch}
 .gbr .doc{background:#fff;border:1px solid var(--rule);padding:20px 24px;display:flex;flex-direction:column;justify-content:space-between;gap:14px;min-height:330px;min-width:0}
diff --git a/src/pages/record/[slug].astro b/src/pages/record/[slug].astro
index d6d2e7b..10e4d95 100644
--- a/src/pages/record/[slug].astro
+++ b/src/pages/record/[slug].astro
@@ -90,6 +90,18 @@ const url = `${SITE}/record/${r.slug}`;
 // 全文（Markdown。Studio から移した記事は HTML）を head に、ファイルの一覧を構造化データ（hasPart）に出して、AI がたどれるようにする
 const filesIndexPath = `public/files/${r.day}/index.json`;
 const filesIndex = existsSync(filesIndexPath) ? JSON.parse(readFileSync(filesIndexPath, 'utf-8')) : null;
+// 記事の素材（分けたファイル）への入口（2026-10-28）。本文の「ファイル一式」の枠に id="files" を付け、タイトルの下から飛べるようにする
+if (filesIndex) body = body.replace('<div class="filebox fb-all">', '<div class="filebox fb-all" id="files">');
+const filesCount = filesIndex && body.includes('id="files"') ? filesIndex.files.length : 0;
+// 古い記録から今の話への道（2026-10-28）。検索から古い記録に着いた人が、そこで止まらないように。
+// 公開から60日より前の記録に出す。同じシリーズに60日以内の記録があればその最新へ（「{シリーズ名}の最新」）、なければサイト全体の最新の記録へ（「いま進めている記録」）
+const DAY_MS = 864e5, OLD_DAYS = 60;
+const fresh = (c) => Date.now() - new Date(c.published).getTime() < OLD_DAYS * DAY_MS;
+const isOld = !isEnglish && Date.now() - new Date(r.published).getTime() >= OLD_DAYS * DAY_MS;
+const seriesNow = isOld ? (await allCards()).find((c) => c.series === r.series && c.slug !== r.slug && !/-en$/.test(c.slug) && fresh(c)) : null;
+const siteNow = isOld && !seriesNow ? (await allCards()).find((c) => !/-en$/.test(c.slug) && c.slug !== r.slug) : null;
+const nowCard = seriesNow || siteNow;
+const ym = (iso) => { const d = new Date(new Date(iso).getTime() + 9 * 3600e3); return `${d.getUTCFullYear()}年${d.getUTCMonth() + 1}月`; };
 const codeLang = { csharp: 'C#', json: 'JSON', yaml: 'YAML', markdown: 'Markdown', python: 'Python', text: 'Text' };
 const jsonLd = [
   {
@@ -129,6 +141,15 @@ const jsonLd = [
     {r.thumb ? <div class="hero"><iframe src={r.thumb} sandbox="allow-scripts" title={r.title} scrolling="no" tabindex="-1"></iframe></div> : r.cover && <img class="hero-img" src={r.cover} alt={r.title} width="1920" height="1080" />}
     <div class="meta"><span class="day">{r.day}</span>{seriesPage ? <a class="tag" href={`/${seriesPage[0]}`}>{r.series}</a> : <span class="tag">{r.series}</span>}<span class="date">{ymd(r.published)}</span></div>
     <h1 class="title">{r.title}</h1>
+    {nowCard && (
+      <a class="now-bar" href={`/record/${nowCard.slug}`} data-ev="今の続き" data-ev-area="now">
+        <span class="nb-k">{ym(r.published)}の記録です。{seriesNow ? `${seriesName}の最新` : 'いま進めている記録'}</span>
+        <span class="nb-t">{nowCard.day}　{nowCard.title}</span><i class="ms" aria-hidden="true">arrow_forward</i>
+      </a>
+    )}
+    {filesCount > 0 && (
+      <a class="files-jump" href="#files" data-ev="素材" data-ev-area="files">この記事の素材（コード・ログ・データ {filesCount}ファイル・まとめてダウンロードあり）<i class="ms" aria-hidden="true">arrow_downward</i></a>
+    )}
     {kind === 'studio'
       ? <div class="richText" set:html={body} />
       : body ? <div class="richText" set:html={body} /> : <div class="richText"><Fragment set:html={toc} /><Content /></div>}
@@ -198,6 +219,16 @@ const jsonLd = [
   .rec .meta { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
   .rec .day { font: 400 28px/28px var(--sans); }
   .rec .title { margin: 12px 0 40px; font: 600 28px/42px var(--sans); overflow-wrap: anywhere; }
+  .rec .title:has(+ .now-bar), .rec .title:has(+ .files-jump) { margin-bottom: 24px; }
+  .now-bar { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 2px 12px; margin: 0 0 24px; padding: 12px 16px; border-radius: 4px; background: var(--ink); color: #fff; text-decoration: none; }
+  .now-bar .nb-k { grid-column: 1; font: 400 13px/20px var(--sans); color: rgba(255,255,255,.72); }
+  .now-bar .nb-t { grid-column: 1; font: 600 15px/24px var(--sans); overflow-wrap: anywhere; }
+  .now-bar .ms { grid-column: 2; grid-row: 1 / span 2; font-size: 20px; transition: transform .2s ease; }
+  .now-bar:hover .ms { transform: translateX(3px); }
+  .files-jump { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 24px; padding: 12px 16px; border-radius: 4px; background: #f0f0f0; color: var(--ink); font: 600 14px/22px var(--sans); text-decoration: none; }
+  .files-jump .ms { font-size: 20px; flex: none; }
+  .files-jump:hover { background: #e6e6e6; }
+  #files { scroll-margin-top: 88px; }
 
   .richText { color: var(--ink); font-family: var(--sans); }
   .richText p { font-size: 18px; line-height: 36px; margin: 8px 0 12px; overflow-wrap: anywhere; }
diff --git a/worker/index.js b/worker/index.js
index 5f8e528..05e7f90 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -230,17 +230,29 @@ async function hkEvent(request, env) {
   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 } });
+  // k：b＝機械（クローラー・自動のブラウザの名乗り、navigator.webdriver）／n＝機械の印なし。2026-10-28 から（それより前は印なし＝o）
+  const k = evKind(request, { wd: d.wd, hl: d.hl, u: 1 }) === 'b' ? 'b' : 'n';
+  await env.MESSAGES.put(key, '', { customMetadata: { e, kind, id, path, w, k } });
   return new Response(null, { status: 204 });
 }
 
 async function hkStats(request, env, url) {
   if (request.method !== 'GET') return new Response('Method Not Allowed', { status: 405 });
+  // who=notbot で機械の印のない記録だけ、who=bot で機械だけ、who=old で印を付ける前（2026-10-28 より前）の記録だけを数える（なければ全部）
+  const WHO = { notbot: 'n', bot: 'b', old: 'o' };
+  const who = WHO[url.searchParams.get('who')] ? url.searchParams.get('who') : '';
   const rows = [];
+  const kinds = new Map();
   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 || {}) });
+    for (const o of r.objects) {
+      const m = o.customMetadata || {};
+      const day = o.key.split('/')[1], k = m.k || 'o';
+      const kv = kinds.get(k) || { shown: 0, click: 0, later: 0 }; kv[m.e] = (kv[m.e] || 0) + 1; kinds.set(k, kv);
+      if (who && k !== WHO[who]) continue;
+      rows.push({ day, ...m });
+    }
     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; };
@@ -248,8 +260,11 @@ async function hkStats(request, env, url) {
   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 KIND_JA = { n: '機械の印なし', b: '機械', o: '印なし（10/28より前）' };
   const data = {
     total: rows.length,
+    who: who || null,
+    byKind: ['n', 'b', 'o'].filter((k) => kinds.has(k)).map((k) => ({ kind: KIND_JA[k], ...kinds.get(k) })),
     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 };
@@ -269,6 +284,8 @@ async function hkStats(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}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>
+<p>${who ? `絞り込み：${esc(who)}（?who= を外すと全部）` : '全部の記録。?who=notbot で機械の印のない記録だけ、?who=bot で機械だけ、?who=old で印を付ける前の記録だけに絞れる'}</p>
+<h2>人と機械（絞り込みに関係なく全部。2026-10-28 から印を付けている）</h2>${table('種類', data.byKind, 'kind')}
 <h2>週ごと（月曜はじまり。週が変わると新しい行が増える）</h2>${table('週', data.byWeek, 'week')}
 <h2>日ごと</h2>${table('日付', data.byDay, 'day')}
 <h2>更新ごと</h2>${table('更新', data.byItem, 'id')}
@@ -282,7 +299,7 @@ h1{font-size:20px}h2{font-size:16px;margin-top:28px}table{border-collapse:collap
 // 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_AREAS = new Set(['hakobu', 'header', 'footer', 'crumbs', 'toc', 'consult', 'pager', 'related', 'body', 'main', 'sim', 'now', 'files', '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 のプレビュー・自動のブラウザ）。名乗りそのものは保存しない
@@ -332,7 +349,7 @@ async function evRecord(request, env, url) {
 // 週は月曜はじまり（日本時間の日付から、その週の月曜の日付を出す）
 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: 'その他' };
+const EV_AREA_JA = { hakobu: 'ハコブくん', header: 'ヘッダー', footer: 'フッター', crumbs: '現在地', toc: '目次', consult: '相談の案内', pager: '前後の記事', related: '関連記事', body: '本文', main: 'ページの中身', now: '今の続き', files: '記事の素材', other: 'その他' };
 
 async function evStats(request, env, url) {
   if (request.method !== 'GET') return new Response('Method Not Allowed', { status: 405 });
