AI に見つけてもらい、正しく引用させる — サイトを機械可読にした
SUZ-LAB のミッション「AI への発信」を、実際に AI が発見・解釈・引用できる形に実装した記録。AI 向けの索引 /llms.txt と全文版 /llms-full.txt、記事ごとの生 Markdown(体裁を剥いだ本文+引用用メタ)、主要 AI クローラを明示許可する robots.txt、パースが確実な JSON Feed 1.1、用語辞書を schema.org の DefinedTermSet で公開する構造化データ——を一式そろえた。値の出所は必ずサイトの唯一の出所(content/*)で、各レイヤーは体裁に写すだけ・文言も URL 規則も二重に持たない、という規律をどう貫いたか。robots で AI クローラを列挙する意味、layout:'full' の逃がし方、最終更新日の定義を一本化した判断まで、実際のコードで残します。
何が問題だったか
SUZ-LAB の Mission は「一次情報として発信する」ことだ。そして 2026 年のいま、情報を集めて回るのは人間だけではない——AI(Claude / ChatGPT / Perplexity …)が読み、要約し、引用する。ミッションを額面どおり受け取るなら、発信先には AI も含まれる。
ところが、人間向けの HTML はそのままでは AI に優しくない。
- 本文にレイアウト・装飾・ナビゲーションが混ざり、本文だけを取り出しにくい。
- サイトのどこに何があるか(目次)が、機械可読な形で提示されていない。
- 「いつ更新されたか」が曖昧だと、AI は鮮度で信頼度を測れない。
そこで、公開 MCP サーバ(構造化データの窓口)に続けて、軽量に発見・取得できる「機械可読レイヤー」を一式そろえることにした。MCP は「接続してくれる AI」向け、こちらは「クロールして回る AI・素朴に URL を叩く AI」向け——両輪だ。
貫いた 1 つの規律
実装は複数のエンドポイントに分かれるが、設計原則は 1 つだけ。
値の出所は必ずサイトの唯一の出所(
content/*と、その読み取り層lib/logs・lib/topics・lib/mcp/content)。各レイヤーは体裁に写すだけで、文言も URL 規則も二重に持たない。
URL は SITE.url(カノニカル)で絶対化し、最終更新日の定義(後述)も 1 か所に集約する。これを守ると、記事を 1 本足すだけで llms.txt・全文版・JSON Feed・sitemap・JSON-LD がすべて自動で追従する。
やってみたこと
1. /llms.txt — AI 向けの索引
llms.txt 慣習に沿った「AI 向け目次」を /llms.txt に出す。サイト名・タグライン・イントロに続けて、VMOST・Roadmap(進行中)・Topics・Logs・Glossary・Feeds を、各正規 URL と一行要約つきで並べる。組み立ては lib/llms.ts の siteLlmsTxt() 一本。
// lib/llms.ts(抜粋)
const lines: string[] = [
`# ${SITE.name}`,
`> ${SITE.tagline}`,
``,
`このページは AI 向けの索引です。構造化データが必要なら公開 MCP サーバを推奨します:`,
`- MCP (Streamable HTTP): ${abs("/api/mcp")}`,
`- 全文版(全記事の本文を連結): ${abs("/llms-full.txt")}`,
// …VMOST / Roadmap(進行中のみ)/ Topics / Logs / Glossary / Feeds
];
冒頭で MCP と全文版へ誘導しているのがポイント。索引だけでは足りない AI に、「構造化データなら MCP、全文が要るなら llms-full.txt」と次の一手を示す。
2. /llms-full.txt — 全文版
索引だけでは本文が要る AI に届かない。/llms-full.txt は目次+全記事の本文を 1 ファイルに連結し、AI がサイト全体をそのままコンテキストに投入できるようにする。記事境界が分かるよう罫線(= × 72)で区切る。
3. 記事ごとの生 Markdown
各記事は /logs/<slug>/llms.txt・/topics/<slug>/llms.txt で、体裁を剥いだ生 Markdown を返す。先頭に、AI が引用に使うメタ(正規 URL・公開/更新日・出典種別・柱・タグ)を付す。
function articleDoc(params) {
return [
`# ${params.title}`,
``,
`> ${params.summary}`,
``,
`- 出典: ${SITE.name}(${params.sourceType})`,
`- URL: ${params.url}`,
`- 公開: ${params.date}`,
`- 更新: ${params.updated}`,
// …柱・タグ・カバー画像
`---`,
params.body,
].join("\n");
}
[slug] 配下に静的セグメント llms.txt を置くことで、/logs/<slug>/llms.txt という「人間向けページの隣に生テキスト版」の関係を URL で表現している。
4. robots.txt — 主要 AI クローラを明示許可する
robots.ts で、主要な AI クローラ(GPTBot・ClaudeBot・PerplexityBot・Google-Extended・CCBot …)を明示的に全面許可する。
const AI_CRAWLERS = [
"GPTBot", "OAI-SearchBot", "ChatGPT-User",
"ClaudeBot", "Claude-Web", "Claude-User", "anthropic-ai",
"PerplexityBot", "Perplexity-User",
"Google-Extended", "CCBot", "cohere-ai", "Applebot-Extended",
];
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: AI_CRAWLERS, allow: "/" },
{ userAgent: "*", allow: "/" },
],
sitemap: `${SITE.url}/sitemap.xml`,
host: SITE.url,
};
}
既定の * でも通るのに、なぜわざわざ列挙するのか——ここは判断だった。理由は、このサイトの方針が「AI への発信」だから、出す/出さないの意思表示を明文化する場にしたかったこと。列挙して明示許可しておけば、将来「この学習クローラには収集を拒否したい」と決めたとき、そのボットだけを disallow に振り分ける切り替え口になる。
5. JSON Feed 1.1 — パースが確実なフィード
RSS(/logs/rss.xml)は既にあるが、XML はパースが揺れる。AI エージェント・自動化と相性がいいのは JSON なので、/logs/feed.json に JSON Feed 1.1 を追加した。拡張フィールド(_ 名前空間)で、生 Markdown への導線も添える。
items: getAllLogs().map((post) => ({
id: `${SITE.url}/logs/${post.slug}`,
title: post.title,
date_published: new Date(post.date).toISOString(),
date_modified: new Date(post.updated ?? post.date).toISOString(),
// JSON Feed の拡張フィールド。AI 向けの生 Markdown への導線を添える
_suzlab: { markdown_url: `${SITE.url}${logMarkdownPath(post.slug)}` },
})),
6. 構造化データ — 用語辞書を DefinedTermSet で公開
用語リンク辞書 glossary を、schema.org の DefinedTermSet として JSON-LD で公開する。各用語が SUZ-LAB で何を指すか(description)と正規 URL を機械可読で示し、AI がサイト固有の語彙を取り違えないようにする。表記ゆれ(terms)が複数あれば先頭を name、残りを alternateName にする。
hasDefinedTerm: GLOSSARY.map((entry) => {
const [name, ...alternates] = entry.terms;
return {
"@type": "DefinedTerm",
name,
...(alternates.length > 0 ? { alternateName: alternates } : {}),
...(entry.description ? { description: entry.description } : {}),
url: `${SITE.url}${entry.href}`,
inDefinedTermSet: setId,
};
}),
ハマったところ・判断
最終更新日の定義を 1 か所に固める
AI と検索エンジンは鮮度で信頼度を測る。ところが構造化データの dateModified は、当初どのページも公開日(date)で固定していた——実質的に書き換えても「更新されていない」ことになっていた。
frontmatter に「最終更新日 updated(省略時は公開日)」を導入し、その定義を 1 つの関数に集約した。
// lib/llms.ts
const lastModified = (meta: { date: string; updated?: string }): string =>
meta.updated ?? meta.date;
この 1 定義を、JSON-LD の dateModified・sitemap の lastModified・JSON Feed の date_modified・生 Markdown 配信の Last-Modified ヘッダ・llms.txt の日付表示——すべての出所にした。「最終更新日とは何か」を二重に持たない。
layout: "full" のトピックは散文にならない
/topics/vmost のような解説は、ページ全体を React コンポーネント(<Framework />)で描画する layout: "full"。この記事は生 Markdown 本文を持たない。そのままでは AI に空の本文を返してしまうので、本文の代わりに「これはコンポーネント描画なので構造化された内容は MCP で取れる」旨を置いて誘導した。空を返すより、次に叩くべき口(MCP・URL)を示すほうが親切だ。
検証
各エンドポイントを実際に叩いて確かめた。
curl -s https://suz-lab.co.jp/llms.txt # 索引。VMOST/Roadmap/Topics/Logs/Glossary/Feeds
curl -s https://suz-lab.co.jp/llms-full.txt # 全文版。罫線で記事境界が分かる
curl -s https://suz-lab.co.jp/logs/<slug>/llms.txt # 生 Markdown+引用メタ
curl -s https://suz-lab.co.jp/logs/feed.json | jq .items[0]._suzlab # markdown_url が付く
curl -sI https://suz-lab.co.jp/logs/<slug>/llms.txt | grep -i last-modified # updated 由来
curl -s https://suz-lab.co.jp/robots.txt # AI クローラ群が明示 allow
記事を 1 本足すと、llms.txt の索引・全文版・JSON Feed・sitemap・JSON-LD にすべて自動で載ること、updated を更新すると全出力の最終更新日が同時に動くことを確認した。
学び
- ミッションを額面どおり実装すると、読者の定義が変わる。 「AI への発信」を本気で受け取れば、発信先に AI クローラや AI エージェントが入る。人間向けの体裁を剥いだ「機械可読レイヤー」を、人間向けサイトと同じ出所から生やすのが素直だった。
- 唯一の出所を決めておくと、レイヤーは無限に増やせる。 llms.txt・全文版・生 Markdown・JSON Feed・JSON-LD——出力形式は増えたが、どれも
content/*に写すだけ。文言も URL 規則も日付定義も二重に持たないので、追加コストが低い。 - 「明示的に許可する」こと自体がメッセージになる。 robots で AI クローラを列挙するのは技術的には不要でも、意思表示と将来の切り替え口として意味がある。設定は挙動だけでなく態度も表す。
- 鮮度は 1 つの定義に集約する。
updated ?? dateを全出力の唯一の出所にしたことで、「最終更新日とは何か」がぶれなくなった。AI が信頼度を測る材料は、こちらが一貫して出して初めて効く。




