commit 227e893c08509aef41a3e5dc64c9b8ee30dcefa5 Author: PixelstoMotors Date: Sun Nov 1 09:57:54 2026 +0900 記事の全文ページ(/record/{slug}.md)と「AIで読む」ボタンを足す - すべての記事に、AI が丸ごと読める Markdown の全文ページを作る(分けたコード・ログも含む) - 全文ページは記事ページを canonical にして返す。head の alternate と llms.txt を全記事でこのページに - 記事のタイトルの下に「AIで読む」:ChatGPT・Claude・Perplexity を質問文入りで開く、Gemini とほかの AI はコピー - 押した数は /api/ev-stats の場所 ai で見る Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LevEXUdawHCPzCgXi4FEM9 diff --git a/package.json b/package.json index c695f91..d4ef6f8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@astrojs/sitemap": "3.7.4", "astro": "5.13.5", "mdast-util-to-string": "4.0.0", + "turndown": "7.2.0", "unist-util-visit": "5.1.0" } } diff --git a/src/pages/llms.txt.ts b/src/pages/llms.txt.ts index f9ce6b3..c4ab6f9 100644 --- a/src/pages/llms.txt.ts +++ b/src/pages/llms.txt.ts @@ -1,19 +1,10 @@ // /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'; +// すべての記録に、全文の Markdown(/record/{slug}.md。src/lib/fulltext.ts。2026-11-01)を添える 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; } -}; +import { fullTextPath } from '../lib/fulltext'; export async function GET() { const cards = await allCards(); @@ -23,7 +14,7 @@ export async function GET() { '', '> ADAPT DESIGN(島津 栄一 / Eiichi Shimazu)による、フィジカルAI・ロボットの研究記録のサイト。机の上のロボットアームでお手本のデータを集め、AI に学習させ、実機で動かして確かめる環境を自宅で組み、誰でも同じ手順で始められる形で公開している。サービスロボット(配膳・搬送・清掃など)の導入前シミュレーションも行っている。', '', - '- 記録(LOG)は作業の過程をそのまま残したもので、コード・実行ログ・数値を全量で載せている。長いコードとログは別ファイルに分け、記事の全文の Markdown を添えている', + '- 記録(LOG)は作業の過程をそのまま残したもので、コード・実行ログ・数値を全量で載せている。長いコードとログは別ファイルに分けている。どの記録にも、コード・ログを含む全文の Markdown(記事の住所の末尾に .md)がある', '- 試算の前提の数字は、公開事例と観察から置いた仮の値。記録が新しいほど前提が更新されている(公開済みの記録は直さない)', '- 言語は日本語', '', @@ -45,10 +36,7 @@ export async function GET() { '', '## 記録(新しい順)', '', - ...cards.map((c) => { - const md = fullMarkdown(c.day); - return `- [${c.day} ${c.title}](${SITE}/record/${c.slug}): ${seriesName(c.series)}${md ? `(全文の Markdown: ${md})` : ''}`; - }), + ...cards.map((c) => `- [${c.day} ${c.title}](${SITE}/record/${c.slug}): ${seriesName(c.series)}(全文の Markdown: ${SITE}${fullTextPath(c.slug)})`), '', '## Optional', '', diff --git a/worker/index.js b/worker/index.js index b4375c2..79d077a 100644 --- a/worker/index.js +++ b/worker/index.js @@ -38,7 +38,14 @@ export default { } catch { /* 数え損ねても転送はする */ } return secured(Response.redirect(to.toString(), 301)); } - const res = await env.ASSETS.fetch(request); + 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 のクローラーが読みに来たか(記録の失敗でページを止めない) try { const bot = crawlerOf(request, url); @@ -332,7 +339,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', 'now', 'files', 'other']); +const EV_AREAS = new Set(['hakobu', 'header', 'footer', 'crumbs', 'toc', 'consult', 'pager', 'related', 'body', 'main', 'sim', 'now', 'files', 'ai', '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 のプレビュー・自動のブラウザ)。名乗りそのものは保存しない