// サイトの入口。ほとんどのページは作っておいた静的ファイル(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) // - /api/kpi-stats 主に見る3つの数字(AI の依頼読み・AI のリンクからの来訪・サイト名の検索)を日ごと・週ごとに見せる(worker/kpi.js。2026-11-06) // - /mcp/{鍵} サイトの計測を Claude につなぐ部品(MCP サーバー。worker/mcp.js。2026-11-06) // - 5分ごとの scheduled SNS の予約投稿の時刻が来ていたら GitHub Actions を起こす(worker/sns_cron.js)。前の日の3つの数字がまだなければ集計して残す(worker/kpi.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'; import { kpiCron, kpiRange, todayJst, addDays } from './kpi.js'; import { handleMcp } from './mcp.js'; import { gscRefresh } from './gsc.js'; export default { async fetch(request, env, ctx) { const url = new URL(request.url); if (url.pathname.startsWith('/mcp/')) return secured(await handleMcp(request, env, url, { evStats, crStats, hkStats })); if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/media/') || url.pathname === '/pixelstomotors' || url.pathname.startsWith('/pixelstomotors/')) { return secured(await route(request, env, url)); } // 古い住所・まちがった住所への訪問を、今のページへ恒久転送する(2026-10-30。クローラーの「返せなかったページ」から) const moved = movedTo(url.pathname); if (moved) { const to = new URL(moved, url); to.search = url.search; // クローラーの記録にも残す(返した結果 301。どの古い住所に来たかが分かるように) try { const bot = crawlerOf(request, url); if (bot) ctx.waitUntil(crRecord(request, env, url, bot, 301).catch(() => {})); } catch { /* 数え損ねても転送はする */ } return secured(Response.redirect(to.toString(), 301)); } let res = await env.ASSETS.fetch(request); // 記事の全文ページ(/record/{slug}.md。2026-11-01)は、検索では記事ページを正とする(canonical)。文字コードも付ける const full = res.ok && url.pathname.match(/^\/record\/([^/]+)\.md$/); if (full) { res = new Response(res.body, res); res.headers.set('Content-Type', 'text/markdown; charset=utf-8'); res.headers.set('Link', `; rel="canonical"`); } // 検索エンジン・AI のクローラーが読みに来たか(記録の失敗でページを止めない) // 記事の「AIで読む」から来た全文ページ(?via=chatgpt など)は、名乗りが一覧にない読み手(Gemini など)も「その他」として残す(2026-11-01) try { // TOP の「このサイトの楽しみ方」から来た /llms.txt?via=… も同じく数える(2026-11-03) const via = (/^\/record\/[^/]+\.md$/.test(url.pathname) || (/^\/record\/[^/.]+$/.test(url.pathname) && url.searchParams.get('via') === 'gemini') || url.pathname === '/llms.txt' || url.pathname === '/llms' || url.pathname === '/llms.html') && AI_VIA.has(url.searchParams.get('via') || '') ? url.searchParams.get('via') : ''; const bot = crawlerOf(request, url) || (via ? 'その他' : null); if (bot) ctx.waitUntil(crRecord(request, env, url, bot, res.status, via).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))); // 前の日の分を作ったとき(1日1回)だけ、Search Console からサイト名の検索の数も取り直す(worker/gsc.js) ctx.waitUntil(kpiCron(env).then(async (m) => { console.log(m); if (m.startsWith('kpi: made')) console.log('gsc:', JSON.stringify(await gscRefresh(env))); }).catch((e) => console.error('kpi-cron:', e.message))); }, };