llms.txt と llms-full.txt をAstroのビルドで自動生成する実装。載せる記事と載せないページの線引き、冒頭の段落から作る要点、テーマ別の入口の載せ方、生成した中身の確かめ方
llms.txt と
この
この/llms.txt と /llms-full.txt を
llms.txt は、記事ページと同じ一覧から毎回作り直すと食い違わない
ビルドで
この<title> や
作り直しに/llms-full.txt の/llms.txt が
17:24:24 ├─ /llms-full.txt (+105ms)
17:24:24 ├─ /llms.txt (+3ms)
...
17:24:25 [build] 254 page(s) built in 3.55sllmstxt.org の提案で必須なのは、最初の見出しだけ
llms.txt の
実装の
| 項目 | 第2版での |
この |
|---|---|---|
| 必須の要素 | H1だけ | サイト名を |
| 「Optional」の |
補足の |
llms-full.txt・サイトマップ・RSSへの |
| 置く場所 | ルートの/docs/llms.txt のような |
ルートに |
| 各ページの |
同じ.md 版をrel="alternate" type="text/markdown" で |
用意していない |
| HTML から |
rel="describedby" の |
まだ |
| llms-full.txt | 提案の |
独自に |
第2版の
主要なAI事業者が llms.txt をどう扱うかは、Google以外は公式な表明が見当たらない
2026年10月4日に
| 事業者 | 確かめた |
llms.txt に |
|---|---|---|
| 検索セントラルの |
AI向けの |
|
| OpenAI | クローラーの |
robots.txt での |
| Anthropic | クローラーの |
robots.txt での |
| Perplexity | クローラーの |
robots.txt・IPの |
OpenAI・Anthropic・Perplexity は、
AIの
載せるのは公開済みの記事とテーマ別の入口、載せないのは下書きと検索に出さないページ
llms.txt に
| ページ | llms.txt | llms-full.txt | 外し方 |
|---|---|---|---|
| 公開済みの |
題・URL・説明文 | 題・URL・日付・カテゴリー・ |
― |
| テーマ別の |
まとめと、 |
同じ | ― |
下書き(draft: true) |
載せない | 載せない | 記事の |
LINEで/tools/ の |
載せない | 載せない | 記事の |
| 記事が |
載せない | 載せない | 一覧の |
下
import { getCollection, type CollectionEntry } from 'astro:content';
export type Post = CollectionEntry<'posts'>;
let cache: Post[] | null = null;
export async function getPosts(): Promise<Post[]> {
if (cache) return cache;
// 下書き(draft: true)は本番ビルドに出さない。npm run dev のときだけ表示して確認できるようにする
const all = await getCollection('posts', (p) => !p.data.draft || import.meta.env.DEV);
cache = all.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
return cache;
}import.meta.env.DEV のastro dev)では/llms.txt を- [ でastro build の
/tools/ のmeta robots と_headers の
llms-full.txt の要点は、最初の見出しより前の段落から取る
llms-full.txt の
/** 記事冒頭の要点(最初の見出しより前の、最初の段落)。なければ説明文 */
export function postSummary(p: Post): string {
const $ = cheerio.load(p.rendered?.html ?? '', null, false);
for (const el of $.root().children().toArray()) {
if ((el as { name?: string }).name === 'h2') break;
if ((el as { name?: string }).name !== 'p') continue;
const t = oneLine($(el).text()).replace(/^この記事の結論\s*[::]\s*/, '');
if (t.length >= 40) return t;
}
return p.data.description;
}要点を
決めごとは
- Markdown ではなく、
描画後の リンクやHTMLから 取る。 太字の 記号を 取り除く 処理を 自分で 書かずに 済みます。 Astroの コンテンツコレクションでは、 rendered.htmlに描画済みの HTMLが 入っています - 「この
記事の 要点と結論:」の 見出し語は 外す。 して 読ませる ときには 意味の 無い 前置きだからです - 40文字に
満たない 段落は 飛ばす。 「こんにちは」のような 書き出しの 短い 段落を、 要点と して 拾わないためです。 最後まで 見つからなければ 説明文を 使います
10月4日の
テーマ別の入口は、まとめとページ内リンクの組で載せる
ブライダルと
/** テーマ別の入口(ブライダル・フィットネスのカテゴリー)。まとめと、テーマごとの要点・ページ内リンク */
function hubs(): string[] {
const out = ['## テーマ別の入口', ''];
for (const h of hubDefs) {
const c = getCategory(h.category);
if (!c) continue;
const url = abs(categoryUrl(c));
out.push(`- [${c.name}(テーマ別の記事一覧)](${url}): ${oneLine(h.intro)}`);
for (const s of h.sections) out.push(` - [${s.title}](${url}#${s.id}): ${oneLine(s.lead)}`);
}
out.push('');
return out;
}hubDefs は、id(#marketing など)を
記事の
実装の全体:ライブラリ1つと、エンドポイント2つ
生成のsrc/lib/llms.ts に置き、src/pages/llms.txt.ts と src/pages/llms-full.txt.ts からsrc/pages/ の .ts ファイルでGET を
// /llms.txt(ビルド時に生成)
import { llmsTxt } from '../lib/llms';
export async function GET() {
return new Response(await llmsTxt(), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}// /llms-full.txt(ビルド時に生成)。全記事の題・URL・説明・要点・よくある質問
import { llmsFullTxt } from '../lib/llms';
export async function GET() {
return new Response(await llmsFullTxt(), { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}静的出力では、Content-Type はpublic/_headers で
/llms.txt
Content-Type: text/plain; charset=utf-8
/llms-full.txt
Content-Type: text/plain; charset=utf-8本体の src/lib/llms.ts はSITE・AUTHOR・COMPANY はcategories などはplainMd は
// /llms.txt と /llms-full.txt の本文(AI の検索・回答システム向けのサイト案内)
// Google 検索は llms.txt を使っていない(公式)。ほかの AI 向けに置いているだけで、主役は通常の HTML と構造化データ
import * as cheerio from 'cheerio';
import { SITE, AUTHOR, COMPANY } from '../config/site';
import { getPosts, isoDate, type Post } from './posts';
import { categories, categoryUrl, getCategory, primaryCategory, ancestorCategories } from './taxonomy';
import { plainMd } from './schema';
import { hubDefs } from '../data/hubs';
const abs = (p: string) => new URL(p, SITE.url).href;
const day = (d: Date) => isoDate(d).slice(0, 10);
const oneLine = (s: string) => s.replace(/\s+/g, ' ').trim();
/** 記事冒頭の要点(最初の見出しより前の、最初の段落)。なければ説明文 */
export function postSummary(p: Post): string {
const $ = cheerio.load(p.rendered?.html ?? '', null, false);
for (const el of $.root().children().toArray()) {
if ((el as { name?: string }).name === 'h2') break;
if ((el as { name?: string }).name !== 'p') continue;
const t = oneLine($(el).text()).replace(/^この記事の結論\s*[::]\s*/, '');
if (t.length >= 40) return t;
}
return p.data.description;
}
function header(): string[] {
return [
`# ${SITE.name}`,
'',
`> ${SITE.description}`,
'',
`${SITE.name}(${abs('/')})は、${COMPANY.name}(${COMPANY.url})の${AUTHOR.jobTitle}・${AUTHOR.name}(${AUTHOR.nameEn})が個人で書いている開発ブログです。`,
`著者は${COMPANY.name}で${AUTHOR.role}しています。${AUTHOR.summary}`,
AUTHOR.aboutBlog,
AUTHOR.regions,
`お仕事のご相談は${COMPANY.name}のお問い合わせ(${COMPANY.contactUrl})で受け付けています。`,
'',
'## 著者と会社',
'',
`- [運営者について(${AUTHOR.name}のプロフィール)](${abs('/about/')})`,
`- [${COMPANY.name}](${COMPANY.url}): 著者が${AUTHOR.jobTitle}を務める会社`,
`- [bundlyze コラムの執筆者ページ](${AUTHOR.profileUrl})`,
`- [X(${AUTHOR.xHandle})](${AUTHOR.x})`,
`- [Instagram(${AUTHOR.instagramHandle})](${AUTHOR.instagram})`,
'',
...hubs(),
];
}
/** テーマ別の入口(ブライダル・フィットネスのカテゴリー)。まとめと、テーマごとの要点・ページ内リンク */
function hubs(): string[] {
const out = ['## テーマ別の入口', ''];
for (const h of hubDefs) {
const c = getCategory(h.category);
if (!c) continue;
const url = abs(categoryUrl(c));
out.push(`- [${c.name}(テーマ別の記事一覧)](${url}): ${oneLine(h.intro)}`);
for (const s of h.sections) out.push(` - [${s.title}](${url}#${s.id}): ${oneLine(s.lead)}`);
}
out.push('');
return out;
}
/** ルートカテゴリごとに記事をまとめる */
function grouped(posts: Post[]) {
const groups = new Map<string, Post[]>();
for (const p of posts) {
const c = primaryCategory(p.data.categories);
const root = c ? (ancestorCategories(c)[0] ?? c) : undefined;
const key = root?.slug ?? '';
groups.set(key, [...(groups.get(key) ?? []), p]);
}
const order = categories.filter((c) => !c.parent).map((c) => c.slug);
return [...groups].sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]));
}
export async function llmsTxt(): Promise<string> {
const posts = await getPosts();
const out = header();
for (const [slug, list] of grouped(posts)) {
const c = getCategory(slug);
out.push(`## 記事:${c?.name ?? 'その他'}`, '');
for (const p of list) out.push(`- [${p.data.title}](${abs(`/${p.data.slug}/`)}): ${oneLine(p.data.description)}`);
out.push('');
}
out.push(
'## Optional',
'',
`- [全記事の要点(llms-full.txt)](${abs('/llms-full.txt')})`,
`- [サイトマップ](${abs('/sitemap-index.xml')})`,
`- [RSS](${abs('/feed/')})`,
'',
);
return out.join('\n');
}
export async function llmsFullTxt(): Promise<string> {
const posts = await getPosts();
const out = header();
out.push('## 記事(新しい順)', '');
for (const p of posts) {
const d = p.data;
const c = primaryCategory(d.categories);
const cats = c ? [...ancestorCategories(c), c].map((x) => x.name).join(' > ') : '';
const modified = d.modified && d.modified > d.date ? d.modified : d.date;
out.push(
`### ${d.title}`,
'',
`- URL: ${abs(`/${d.slug}/`)}`,
`- 公開日: ${day(d.date)} / 最終更新日: ${day(modified)}`,
...(cats ? [`- カテゴリー: ${cats}(${abs(categoryUrl(c!))})`] : []),
`- 著者: ${AUTHOR.name}(${COMPANY.name} ${AUTHOR.jobTitle})`,
`- 説明: ${oneLine(d.description)}`,
'',
`要点: ${postSummary(p)}`,
'',
);
if (d.faq.length) {
out.push('よくある質問:', '');
for (const f of d.faq) out.push(`- Q. ${oneLine(f.q)}`, ` A. ${oneLine(plainMd(f.a))}`);
out.push('');
}
}
return out.join('\n');
}2つの
- llms.txt は
目次。 記事は題と 説明文だけにし、 カテゴリーの 大分類ごとに H2で 区切ります。 エージェントが 全体を 見渡して、 読む 記事を 選ぶための ファイルです - llms-full.txt は
要約の 記事ごとに束。 日付・カテゴリーの 階層・要点・よく ある 質問まで 載せます。 新しい 順に 並べ、 最終更新日も 入れて、 情報の 新しさが 分かるように しています
URLは new URL(path, SITE.url).href で
ビルドで出力された中身
10月4日にnpm run build をdist/llms.txt はdist/llms-full.txt は
llms.txt の
# やまやまブログ
> 株式会社bundlyze CTO・津嘉山 洸の開発ブログ。Web・システム開発の実装メモから、エンジニアのキャリアまで。
やまやまブログ(https://www.yamayamabloglink.com/)は、株式会社bundlyze(https://www.bundlyze.co.jp/)のCTO・津嘉山 洸(Ko Tsukayama)が個人で書いている開発ブログです。
...
## 著者と会社
- [運営者について(津嘉山 洸のプロフィール)](https://www.yamayamabloglink.com/about/)
- [株式会社bundlyze](https://www.bundlyze.co.jp/): 著者がCTOを務める会社
...
## テーマ別の入口
- [ブライダル(テーマ別の記事一覧)](https://www.yamayamabloglink.com/category/bridal/): 結婚式場・ホテルの婚礼部門・フォトスタジオ・ドレスショップのWeb集客、…
- [集客と検索](https://www.yamayamabloglink.com/category/bridal/#marketing): 結婚式場がフェア予約につながる人に…
- [公式サイトとCMS](https://www.yamayamabloglink.com/category/bridal/#website): 式場の公式サイトで、…
...
## 記事:IT
- [ホームページ制作を依頼する前に社内で決めておくこと。…](https://www.yamayamabloglink.com/website-production-internal-decisions-before-ordering/): ホームページ制作を依頼する前に、…
...
## Optional
- [全記事の要点(llms-full.txt)](https://www.yamayamabloglink.com/llms-full.txt)
- [サイトマップ](https://www.yamayamabloglink.com/sitemap-index.xml)
- [RSS](https://www.yamayamabloglink.com/feed/)llms-full.txt の
### AIクローラーのrobots.txt設定例。GPTBot・ClaudeBot と Google-Extended・PerplexityBotを学習用と検索用に分けて書き、Cloudflare側の設定まで確かめる
- URL: https://www.yamayamabloglink.com/ai-crawler-robots-txt-cloudflare/
- 公開日: 2026-10-03 / 最終更新日: 2026-10-03
- カテゴリー: IT > 技術ブログ(https://www.yamayamabloglink.com/category/it/%E6%8A%80%E8%A1%93%E3%83%96%E3%83%AD%E3%82%B0/)
- 著者: 津嘉山 洸(株式会社bundlyze CTO)
- 説明: AIクローラーをrobots.txtで扱う設定例です。…
要点: AIクローラーを robots.txt で扱うときは、各社の読み取りを「学習用」「検索用」「利用者の依頼でその場で読みに来るもの」の3つに分け、…
よくある質問:
- Q. GPTBotだけ拒否すれば、ChatGPTの検索の答えには今までどおり出ますか?
A. OpenAIの説明では、検索の答えに表示するための読み取りは OAI-SearchBot が担い、…本文の
生成物は、ビルドのあとにスクリプトで点検する
生成した/tools/ の
// ビルド後の dist/llms.txt・llms-full.txt を点検する
// 使い方: node check-llms.mjs <サイトのルート(package.json がある場所)>
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
const root = process.argv[2] ?? '.';
const dist = join(root, 'dist');
const HOST = 'https://www.yamayamabloglink.com';
const llms = readFileSync(join(dist, 'llms.txt'), 'utf8');
const full = readFileSync(join(dist, 'llms-full.txt'), 'utf8');
const sitemap = readFileSync(join(dist, 'sitemap-0.xml'), 'utf8');
const problems = [];
// 1. llms.txt のリンク先が、ビルド結果に実在するか
const links = [...llms.matchAll(/\]\((https?:\/\/[^)]+)\)/g)].map((m) => m[1]);
const own = links.filter((u) => u.startsWith(HOST));
for (const u of own) {
const path = decodeURI(new URL(u).pathname);
const file = path.endsWith('/') ? join(dist, path, 'index.html') : join(dist, path);
if (!existsSync(file)) problems.push(`リンク切れ: ${u}`);
}
// 2. 下書きの記事が混ざっていないか
const postsDir = join(root, 'src/content/posts');
const drafts = [];
for (const dir of readdirSync(postsDir)) {
const md = join(postsDir, dir, 'index.md');
if (!existsSync(md)) continue;
const fm = readFileSync(md, 'utf8').split('\n---')[0];
if (/^draft:\s*true/m.test(fm)) drafts.push(fm.match(/^slug:\s*"?([^"\n]+)"?/m)[1]);
}
for (const s of drafts) {
if (llms.includes(`/${s}/`) || full.includes(`/${s}/`)) problems.push(`下書きが載っている: ${s}`);
}
// 3. 検索に出さないページ(/tools/)が混ざっていないか
if (/yamayamabloglink\.com\/tools\//.test(llms + full)) problems.push('/tools/ が載っている');
// 4. サイトマップで lastmod を持つURL(=記事)が、すべて llms.txt に載っているか
const articles = [...sitemap.matchAll(/<url><loc>([^<]+)<\/loc><lastmod>/g)].map((m) => m[1]);
const missing = articles.filter((u) => !llms.includes(`(${u})`));
for (const u of missing) problems.push(`記事が llms.txt に無い: ${u}`);
// 5. 要点が説明文の使い回し(冒頭の段落が取れなかった)になっている記事
const blocks = full.split('\n### ').slice(1);
const fallback = blocks.filter((b) => {
const desc = b.match(/^- 説明: (.*)$/m)?.[1];
const sum = b.match(/^要点: (.*)$/m)?.[1];
return desc && sum === desc;
});
const lens = blocks.map((b) => (b.match(/^要点: (.*)$/m)?.[1] ?? '').length).sort((a, b) => a - b);
console.log(`llms.txt: ${llms.length.toLocaleString()}文字 / 自サイトへのリンク ${own.length}件(うち記事 ${articles.length}件)`);
console.log(`llms-full.txt: ${full.length.toLocaleString()}文字 / 記事 ${blocks.length}件`);
console.log(`下書き: ${drafts.length}件(載っていないかを確認)`);
console.log(`要点を説明文で代用した記事: ${fallback.length}件`);
console.log(`要点の長さ: 最短${lens[0]}字 / 中央${lens[Math.floor(lens.length / 2)]}字 / 最長${lens.at(-1)}字`);
console.log(problems.length ? `問題 ${problems.length}件\n- ${problems.join('\n- ')}` : '問題なし');
process.exit(problems.length ? 1 : 0);このlastmod をlastmod が
$ node check-llms.mjs .
llms.txt: 43,921文字 / 自サイトへのリンク 186件(うち記事 167件)
llms-full.txt: 212,269文字 / 記事 167件
下書き: 8件(載っていないかを確認)
要点を説明文で代用した記事: 0件
要点の長さ: 最短87字 / 中央257字 / 最長429字
問題なし自サイトへの
誰が取りに来ているかは、記録の準備を終えたところ
llms.txt を/llms.txt と /llms-full.txt への
保存先を
会社の
動作確認した環境
2026年10月4日に、
| 項目 | バージョン・内容 |
|---|---|
| OS | macOS 26 |
| Node.js | 22.23.2 |
| Astro | 7.3.5output: 'static'、trailingSlash: 'always') |
| cheerio | 1.2.0 |
| 実行した |
npm run build(表紙のastro build)、/llms.txt の/llms.txt と /llms-full.txt が |
点検のrel="describedby" の
参照した公式ドキュメント(確認日:2026年10月4日)
- llms.txt
(llmstxt.org) :提案者と日付、 必須は H1だけである こと、 各要素の 順番、 置く 場所と いちばん近い ファイルが 使われる こと、 Markdown版の URL - llms.txt「Changes in v2」:第2版の
変更点 ( rel="alternate"・rel="describedby"、Optional に 仕組みと しての 意味が 無くなった こと、 文脈を 組み立てる 道具を 外した こと、 エージェントの 使い方) - Google 検索セントラル
「生成AI機能向けの :AI向けの最適化ガイド」 ファイルを 作る 必要は 無く、 Google検索は 使わない こと - OpenAI
「Bots」 :OAI-SearchBot・GPTBot・ChatGPT-User などと(クローラーの 説明) robots.txt での 制御 - Anthropic「Does Anthropic crawl data from the web, and how can site owners block the crawler?」:ClaudeBot・Claude-User・Claude-SearchBot と
robots.txt での 制御 - Perplexity「Perplexity Crawlers」:PerplexityBot・Perplexity-User と
robots.txt・IPの 範囲・WAFでの 制御 - Astro Docs「Endpoints」:静的出力で
src/pages/の.tsからGETでファイルを 書き出す 仕組み - Cloudflare Pages「Headers」:
_headersでパスごとに 応答ヘッダーを 付ける 方法
よくある質問
llms.txt は手で書くのと、ビルドで作るのと、どちらがよいですか?
記事や
下書きの記事が llms.txt に載らないようにするには、どうすればよいですか?
記事の
llms-full.txt は置かないといけないものですか?
置く
llms.txt を置けば、ChatGPTやClaudeが読んでくれますか?
そうとは