チュートリアル · 読了 10 分

Next.js / Astro / Hugo で llms.txt をビルド時に生成する

各フレームワークで動く最小コードと、sitemap と共有する 1 つの一覧。

3 つのフレームワークのどれにも llms.txt の規約はありません。ただしどれにも、ページを描画しているのと 同じデータからビルド時に任意のテキストファイルを書き出す 仕組みがあり、ページの一覧を載せるファイルに必要なのは まさにそれです。この記事はそのためのチュートリアルで、 ルートハンドラ 1 つ、エンドポイント 1 つ、出力フォーマット 1 つを扱います。

Next.js の例はこのサイトが実際に動かしているコードです。Astro と Hugo の例は各フレームワークの現行ドキュメントに従っています。 最後の節がファイルを正直に保つ部分で、sitemap と同じ一覧から生成する方法です。

30 秒で分かる結論:Next.js / Astro / Hugo の llms.txt は sitemap と同じ一覧から生成する

Next.js では app/llms.txt/route.ts のルートハンドラがテキストを返します。export const dynamic = "force-static" を足してビルド時に一度だけ描画させます。Astro では src/pages/llms.txt.ts のエンドポイントが GET をエクスポートして Response を返し、静的ビルドでは dist/llms.txt に書き出されます。Hugo では、名前 llms、baseName llms、メディアタイプ text/plain のカスタム出力フォーマットをホームページの outputs に追加し、layouts/index.llms.txt が /llms.txt として描画されます。

生成そのものは簡単な部分です。大事な判断は中身で、すでに sitemap を作っている一覧を再利用し、初めての訪問者に見せるセクションだけに 絞って、リンクごとに一文を書きます。小さなサイトなら public/ に手書きのファイルを置けば十分で、下の コードは手書きでは古くなるくらいページが増えるサイト向けです。

手書きの静的ファイルか、生成か

Next.jsAstroHugo
手書きファイルpublic/llms.txtpublic/llms.txtstatic/llms.txt
ビルド時に生成app/llms.txt/route.ts + force-staticsrc/pages/llms.txt.ts のエンドポイントホームページのカスタム出力フォーマット
データの元自前のルート一覧か CMS の取得astro:content の getCollectionテンプレート内の .Site.RegularPages
Content-Type の制御Response のヘッダで指定静的ビルドではホストが拡張子で決めるホストが拡張子で決める
組み込みの規約なし(robots.ts と sitemap.ts はあるが llms.txt はない)なし(コミュニティ製インテグレーションはある)なし(要望 Issue は重複としてクローズ)

Next.js:force-static のルートハンドラ

このサイトが /llms.txt で配信しているファイルと同じ形です。llms.txt という名前のフォルダはルートセグメントとして有効で、ハンドラは Markdown の本文と必要なヘッダを持つ素の Response を返します。

// app/llms.txt/route.ts
import { ROUTES } from "@/lib/routes";

export const dynamic = "force-static";

export function GET() {
  const lines = [
    "# Acme",
    "",
    "> Acme は小規模クリニック向けの予約管理ツールです。以下のページで初期設定、料金、連携を説明しています。",
    "",
    "## ガイド",
    ...ROUTES.filter((r) => r.section === "guides").map(
      (r) => `- [${r.title}](https://acme.example${r.path}): ${r.description}`,
    ),
    "",
    "## Optional",
    "- [変更履歴](https://acme.example/changelog): リリースノート。新しい順。",
    "",
  ];
  return new Response(lines.join("\n"), {
    headers: {
      "Content-Type": "text/markdown; charset=utf-8",
      "Cache-Control": "public, max-age=3600, s-maxage=86400",
    },
  });
}

補足を 2 つ。Next.js 15 以降、GET ハンドラは既定で動的なので、force-static がないとリクエストのたびに描画されます。付ければビルドが一度だけ 書き出します。もう 1 つ、App Router には robots.ts や sitemap.ts のような llms.txt の規約はないので、ルートハンドラが正規の手段です。Next.js のドキュメントサイト自身も /docs/llms.txt を同じやり方で配信しています。

Astro:コンテンツコレクションを読むエンドポイント

src/pages/ 配下の、名前が .txt.ts で終わる .ts ファイルは .txt の URL になります。静的ビルドでは Astro がエンドポイントを一度呼び、 結果を dist/llms.txt に書き出します。

// src/pages/llms.txt.ts
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";

export const GET: APIRoute = async ({ site }) => {
  const docs = (await getCollection("docs", ({ data }) => !data.draft))
    .sort((a, b) => a.data.order - b.data.order);

  const body = [
    "# Acme",
    "",
    "> Acme は小規模クリニック向けの予約管理ツールです。以下のページで初期設定、料金、連携を説明しています。",
    "",
    "## ガイド",
    ...docs.map((d) => `- [${d.data.title}](${new URL(`/docs/${d.id}/`, site)}): ${d.data.description}`),
    "",
  ].join("\n");

  return new Response(body, {
    headers: { "Content-Type": "text/markdown; charset=utf-8" },
  });
};

site の値は astro.config から来て、これがリンクを絶対 URL にします。draft の絞り込みは sitemap インテグレーションが使うのと同じ規則なので、 2 つのファイルの「公開済み」が一致します。ヘッダが意味を持つのは サーバー描画の出力だけで、静的ホストでは拡張子が決めます。

Hugo:ホームページのカスタム出力フォーマット

Hugo はどの種類のページも好きな数のフォーマットに描画できます。 llms.txt 用のフォーマットを定義し、ホームページの outputs に加え、そのテンプレートを書きます。text/plain は txt の拡張子を持つ組み込みのメディアタイプで、 これがファイル名を llms.txt にします。

# hugo.toml
[outputFormats.llms]
  mediaType = "text/plain"
  baseName = "llms"
  isPlainText = true
  notAlternative = true

[outputs]
  home = ["html", "rss", "llms"]
{{/* layouts/index.llms.txt */}}
# {{ .Site.Title }}

> {{ .Site.Params.description }}

## ガイド
{{ range where .Site.RegularPages "Section" "guides" }}
- [{{ .Title }}]({{ .Permalink }}): {{ .Description | default .Summary | plainify }}
{{- end }}

## Optional
{{ range first 10 (where .Site.RegularPages "Section" "posts") }}
- [{{ .Title }}]({{ .Permalink }}): {{ .Description | default .Summary | plainify }}
{{- end }}

outputs.home を設定すると既定の一覧が置き換わるので、html と rss を残します。notAlternative は、テーマが HTML の head に出すことのある rel=alternate のリンクからこのファイルを外します。.Site.RegularPages は下書きと未来日付のコンテンツをすでに除いているので、Hugo が sitemap に載せるものと一覧が一致します。

sitemap と同期させる

手書きの llms.txt の問題は初日に間違っていることではなく、半年後に 間違っていることです。直し方は構造で、ルートの一覧を 1 つにして 両方のファイルがそれを使うようにします。

  • Next.js:各ページの path、title、description、 section を持つ lib/routes.ts を用意します。app/sitemap.ts はそれを sitemap の項目に、app/llms.txt/route.ts は絞り込んだ一部を Markdown の行に変換します。ページを足すときはオブジェクトを 1 つ足すだけです。
  • Astro:sitemap インテグレーションもエンドポイントも 同じコンテンツコレクションを見るので、共有の元はすでにあります。 両方に同じ draft の絞り込みをかけ、すべての項目の frontmatter に description を書きます。エンドポイントはそれをリンクの注釈に使います。
  • Hugo:.Site.RegularPages が組み込みの sitemap テンプレートにも自作の llms テンプレートにも渡ります。注釈には front matter の description を、対象の選択にはパスの手書き一覧では なくセクションの絞り込みを使います。

一覧を共有することは sitemap を写すことではありません。sitemap は全部を載せ、llms.txt は初めての読者に必要なセクションを、 説明する順に、一文ずつ添えて載せます。絞り込みと説明文が 編集の仕事で、ファイルを丸ごと吐き出すのではなくデータから 生成する理由もそこにあります。

確認の仕方

curl -sI https://acme.example/llms.txt | grep -i -E 'HTTP/|content-type'
curl -s https://acme.example/llms.txt | head -8
curl -s https://acme.example/llms.txt | grep -c '^- \['

見たいのは、200 であること、text/ 系の Content-Type であること、1 行目が H1 で、引用ブロックが説明として読めること、 リンク数が数百ではなく数十であることです。そのあとバリデータに 通せば、絶対 URL のつもりで相対になっているリンクや末尾の改行の 欠落も拾えます。

生成したファイルを検証する

サイトの URL を貼ると、/llms.txt を取得して仕様に沿った構造かを確認し、見つかったリンクをすべて一覧にします。ビルドが意図どおりのものを出したかがそのまま分かります。

バリデータを開く →

よくある質問

public フォルダに llms.txt を置くだけではだめですか?

だめではなく、年に数回しか変わらない小さなサイトならそれが正解です。Next.js と Astro は public/llms.txt、Hugo は static/llms.txt に置けばそのまま配信されます。生成に切り替えるのは、手書きのファイルが古くなるくらいの頻度でページが増えるとき、あるいは同じ一覧がすでに sitemap を作っていて 2 つ目のコピーを持ちたくないときです。

Next.js には robots.ts や sitemap.ts のような llms.txt の規約がありますか?

ありません。2026 年 9 月時点で App Router のメタデータファイル規約は robots.txt、sitemap.xml、アイコン、Open Graph 画像で、llms.txt は含まれていません。追加の要望は Next.js リポジトリの Discussion として開いています。app/llms.txt/route.ts のルートハンドラで十分で、Next.js のドキュメントサイト自身も /docs/llms.txt を同じ考え方で配信しています。

Next.js の llms.txt がリクエストのたびに実行されるのはなぜですか?

Next.js 15 以降、GET のルートハンドラは既定で動的だからです。ルートファイルに export const dynamic = 'force-static' を足すと、レスポンスはビルド時に一度だけ描画されます。デプロイのときにしか変わらないファイルにはそれで十分です。

Astro のエンドポイントで設定したレスポンスヘッダは使われますか?

サーバーで描画する出力でだけ使われます。静的ビルドではエンドポイントが一度実行され、本文が dist/llms.txt に書き出されます。ホスティング側はそのファイルを拡張子で決めた Content-Type、たいていは text/plain で配信します。それで問題ありません。text/markdown にしたい場合はエンドポイントではなくホストのヘッダ設定で指定します。

Hugo の出力フォーマットにはどのメディアタイプを使うべきですか?

text/plain です。Hugo に組み込まれていて、ファイルに .txt の拡張子を与えます。text/markdown を使うと、Hugo はメディアタイプから拡張子を取るので生成されるのは llms.md になってしまいます。静的ホストでの Content-Type はどのみち拡張子で決まるので、Netlify でも GitHub Pages でも .txt なら text/plain になります。

生成するファイルは sitemap のように全ページを載せるべきですか?

いいえ。sitemap は網羅のためのもので、llms.txt はリンクごとに一文を添えた選び抜かれた索引です。同じ元の一覧を使い、そこから絞り込みます。初めての訪問者に見せるセクションだけ残し、ページ送りやタグページ、法務ページは落とし、各項目に説明を書きます。注釈つきのリンク 40 本のほうが、注釈なしの 400 本よりアシスタントの役に立ちます。

次に読む