# 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（実験ログ / 一次情報）
- URL: https://suz-lab.co.jp/logs/making-suz-lab-machine-readable
- 公開: 2026-07-09
- 更新: 2026-07-09
- 著者: Hiro（主任研究員・https://suz-lab.co.jp/researchers/hiro・X: https://x.com/suz_lab_hiro）
- 柱: テクノロジー
- タグ: #AI #llms.txt #JSON Feed #JSON-LD #SEO #Next.js #TypeScript #単一の出所
- カバー画像: https://suz-lab.co.jp/assets/logs/making-suz-lab-machine-readable.png?v=5b0542c2

---


## 何が問題だったか

SUZ-LAB の Mission は「一次情報として発信する」ことだ。そして 2026 年のいま、情報を集めて回るのは人間だけではない——AI（Claude / ChatGPT / Perplexity …）が読み、要約し、引用する。ミッションを額面どおり受け取るなら、**発信先には AI も含まれる**。

ところが、人間向けの HTML はそのままでは AI に優しくない。

- 本文にレイアウト・装飾・ナビゲーションが混ざり、**本文だけを取り出しにくい**。
- サイトのどこに何があるか（目次）が、機械可読な形で提示されていない。
- 「いつ更新されたか」が曖昧だと、AI は鮮度で信頼度を測れない。

そこで、[公開 MCP サーバ](/logs/building-the-public-mcp-server)（構造化データの窓口）に続けて、**軽量に発見・取得できる「機械可読レイヤー」を一式そろえる**ことにした。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()` 一本。

```ts
// 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・公開/更新日・出典種別・柱・タグ）を付す。

```ts
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` …）を**明示的に全面許可**する。

```ts
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 への導線も添える。

```ts
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](/logs/cross-site-glossary-and-auto-linking) を、schema.org の `DefinedTermSet` として JSON-LD で公開する。各用語が SUZ-LAB で何を指すか（`description`）と正規 URL を機械可読で示し、AI がサイト固有の語彙を取り違えないようにする。表記ゆれ（`terms`）が複数あれば先頭を `name`、残りを `alternateName` にする。

```ts
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 つの関数に集約**した。

```ts
// 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）を示すほうが親切だ。

## 検証

各エンドポイントを実際に叩いて確かめた。

```bash
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 が信頼度を測る材料は、こちらが一貫して出して初めて効く。

