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 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 = [

これからロボットAIの学習環境を用意する、
企業の担当者・研究者・エンジニア向け

ロボットAIの
学習用GPU、
買うか借りるか

-

買う前に借りて、実際に学習させました。見積書と、利用明細を並べます。実費と計算機で、自分の場合どちらが合うかを決められる導入ガイドです。

+

結論:使うのが月180時間より少ないなら、借りる方が安い。RTX 5090のPCを4年使う場合の目安です。買うなら、GPUのメモリは24GB以上が条件です。実際に借りて学習させた実費(7回で4,677円)と、自分の条件で確かめられる計算機を置いています。

@@ -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('
', '
'); +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 ?
: r.cover && {r.title}}
{r.day}{seriesPage ? {r.series} : {r.series}}{ymd(r.published)}

{r.title}

+ {nowCard && ( + + {ym(r.published)}の記録です。{seriesNow ? `${seriesName}の最新` : 'いま進めている記録'} + {nowCard.day} {nowCard.title} + + )} + {filesCount > 0 && ( + この記事の素材(コード・ログ・データ {filesCount}ファイル・まとめてダウンロードあり) + )} {kind === 'studio' ?
: body ?
:
} @@ -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) { ハコブくんの知らせの記録

ハコブくんの知らせの記録

サイトの更新を知らせてハコブくんが走った回数と、そのあと見た人がどう動いたか。数だけを残しており、誰かは分からない。日付は日本時間。

+

${who ? `絞り込み:${esc(who)}(?who= を外すと全部)` : '全部の記録。?who=notbot で機械の印のない記録だけ、?who=bot で機械だけ、?who=old で印を付ける前の記録だけに絞れる'}

+

人と機械(絞り込みに関係なく全部。2026-10-28 から印を付けている)

${table('種類', data.byKind, 'kind')}

週ごと(月曜はじまり。週が変わると新しい行が増える)

${table('週', data.byWeek, 'week')}

日ごと

${table('日付', data.byDay, 'day')}

更新ごと

${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 });