commit 227e893c08509aef41a3e5dc64c9b8ee30dcefa5
Author: PixelstoMotors <studio@adaptdesign.jp>
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 <noreply@anthropic.com>
    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', `<https://pixelstomotors.com/record/${full[1]}>; 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 のプレビュー・自動のブラウザ）。名乗りそのものは保存しない
