SUZ-LAB

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/logslib/topicslib/mcp/content)。各レイヤーは体裁に写すだけで、文言も URL 規則も二重に持たない。

URL は SITE.url(カノニカル)で絶対化し、最終更新日の定義(後述)も 1 か所に集約する。これを守ると、記事を 1 本足すだけで llms.txt・全文版・JSON Feed・sitemap・JSON-LD がすべて自動で追従する。

やってみたこと

1. /llms.txt — AI 向けの索引

llms.txt 慣習に沿った「AI 向け目次」を /llms.txt に出す。サイト名・タグライン・イントロに続けて、VMOSTRoadmap(進行中)・TopicsLogs・Glossary・Feeds を、各正規 URL と一行要約つきで並べる。組み立ては lib/llms.tssiteLlmsTxt() 一本。

// 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 クローラ(GPTBotClaudeBotPerplexityBotGoogle-ExtendedCCBot …)を明示的に全面許可する。

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 が信頼度を測る材料は、こちらが一貫して出して初めて効く。
#AI#llms.txt#JSON Feed#JSON-LD#SEO#Next.js#TypeScript#単一の出所

同じ柱のほかのログ

この柱の Roadmap を見る