面向无头 CMS 的 llms.txt

无头 CMS 平台(Contentful、Sanity、Strapi、Directus)不直接提供文件;你可以在构建时根据 CMS 数据生成 llms.txt,或通过服务器路由在请求时获取。

最近更新:

使用托管式网站构建器而不是无头 CMS?请参阅以下平台的专门指南: Shopify, Webflow, WixSquarespace.

核心模式:CMS API → 构建步骤 → 静态文件

Headless CMS 平台负责管理内容,但不能在任意路径提供任意文件。要发布 llms.txt,你需要从 CMS 拉取内容并自行写入文件,方式可以是 构建时 (静态输出)或在 请求时 (服务端路由)。

  1. 查询 CMS,获取带有标题、slug 和摘要字段的已发布页面。
  2. 映射为 Markdown 链接,将每个条目格式化为 - [Title](https://url/): description.
  3. 写入或返回文件,写入 public/llms.txt 在构建时写入文件,或在请求时通过服务器路由返回。

Contentful 示例

使用 Contentful JavaScript SDK 通过 Content Delivery API 获取条目。在构建步骤中运行此脚本(例如在 package.json scripts 或你的 CI 流水线)在框架构建之前:

scripts/generate-llms-txt.mjs (Contentful)
// scripts/generate-llms-txt.mjs
// Contentful: fetch published doc entries and generate llms.txt at build time

import { createClient } from 'contentful';
import { writeFileSync } from 'fs';

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
});

async function generateLlmsTxt() {
  // Fetch entries of content type 'docPage' sorted by display order
  const entries = await client.getEntries({
    content_type: 'docPage',
    order: 'fields.order',
    select: 'fields.title,fields.slug,fields.summary',
    limit: 100,
  });

  const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

  const links = entries.items
    .map((entry) => {
      const { title, slug, summary } = entry.fields;
      return `- [${title}](${SITE_URL}/docs/${slug}/): ${summary ?? ''}`;
    })
    .join('\n');

  const content = [
    '# Your Site',
    '',
    '> One-sentence description of your product.',
    '',
    '## Documentation',
    '',
    links,
    '',
    '## Optional',
    '',
    `- [Changelog](${SITE_URL}/changelog/): Release history.`,
  ].join('\n');

  writeFileSync('public/llms.txt', content, 'utf-8');
  console.log(`Generated llms.txt with ${entries.items.length} entries.`);
}

generateLlmsTxt().catch(console.error);

将脚本添加到你的构建流水线:

package.json
{
  "scripts": {
    "prebuild": "node scripts/generate-llms-txt.mjs",
    "build": "next build"
  }
}

Sanity 示例

使用 Sanity 的查询语言 GROQ 精确获取所需字段。该 !(_id in path("drafts.**")) 过滤器可确保仅包含已发布的文档:

scripts/generate-llms-txt.mjs (Sanity)
// scripts/generate-llms-txt.mjs
// Sanity: use GROQ to query published docs and generate llms.txt

import { createClient } from '@sanity/client';
import { writeFileSync } from 'fs';

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET || 'production',
  useCdn: false, // always fetch fresh data at build time
  apiVersion: '2024-01-01',
});

async function generateLlmsTxt() {
  // GROQ query: fetch all published doc entries with title, slug, and summary
  const docs = await client.fetch(
    `*[_type == "doc" && !(_id in path("drafts.**"))] | order(order asc) {
      title,
      "slug": slug.current,
      summary
    }`
  );

  const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

  const links = docs
    .map((doc) => `- [${doc.title}](${SITE_URL}/docs/${doc.slug}/): ${doc.summary ?? ''}`)
    .join('\n');

  const content = [
    '# Your Site',
    '',
    '> One-sentence description of your product.',
    '',
    '## Documentation',
    '',
    links,
  ].join('\n');

  writeFileSync('public/llms.txt', content, 'utf-8');
  console.log(`Generated llms.txt with ${docs.length} docs.`);
}

generateLlmsTxt().catch(console.error);

Strapi 示例

Strapi 在 /api/:collection。使用 fieldsfilters 查询参数,仅获取已发布且包含所需字段的文档:

scripts/generate-llms-txt.mjs (Strapi)
// scripts/generate-llms-txt.mjs
// Strapi v4/v5: fetch published articles via REST API and generate llms.txt

import { writeFileSync } from 'fs';

const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337';
const STRAPI_TOKEN = process.env.STRAPI_API_TOKEN;
const SITE_URL = process.env.SITE_URL || 'https://yoursite.com';

async function generateLlmsTxt() {
  // Fetch published docs, adjust the collection slug and fields as needed
  const res = await fetch(
    `${STRAPI_URL}/api/docs?fields[0]=title&fields[1]=slug&fields[2]=summary&filters[publishedAt][$notNull]=true&pagination[limit]=100`,
    {
      headers: STRAPI_TOKEN ? { Authorization: `Bearer ${STRAPI_TOKEN}` } : {},
    }
  );

  if (!res.ok) throw new Error(`Strapi API error: ${res.status}`);
  const { data } = await res.json();

  const links = data
    .map((item) => {
      const { title, slug, summary } = item.attributes ?? item; // v4 vs v5
      return `- [${title}](${SITE_URL}/docs/${slug}/): ${summary ?? ''}`;
    })
    .join('\n');

  const content = [
    '# Your Site',
    '',
    '> One-sentence description of your product.',
    '',
    '## Documentation',
    '',
    links,
  ].join('\n');

  writeFileSync('public/llms.txt', content, 'utf-8');
  console.log(`Generated llms.txt with ${data.length} entries.`);
}

generateLlmsTxt().catch(console.error);

Strapi v4 将响应数据封装在 attributes 对象;Strapi v5 在顶层返回字段。该示例通过回退机制兼容两种情况。

与 Next.js 路由处理程序集成

如果您希望文件始终保持同步且无需完整重建,请使用带有 revalidate 设置为所需的 TTL。Next.js 将缓存响应,并 在后台重新生成:

app/llms.txt/route.ts (Next.js + Contentful)
// app/llms.txt/route.ts
// Next.js App Router, fetch from CMS at request time (or cache with revalidate)

import { NextResponse } from 'next/server';
import { createClient } from 'contentful';

// Cache for 1 hour (Next.js incremental static regeneration)
export const revalidate = 3600;

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
});

export async function GET() {
  const entries = await client.getEntries({
    content_type: 'docPage',
    order: 'fields.order',
    select: 'fields.title,fields.slug,fields.summary',
    limit: 100,
  });

  const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yoursite.com';

  const links = entries.items
    .map((e: any) => `- [${e.fields.title}](${SITE_URL}/docs/${e.fields.slug}/): ${e.fields.summary ?? ''}`)
    .join('\n');

  const body = [
    '# Your Site',
    '',
    '> One-sentence description.',
    '',
    '## Documentation',
    '',
    links,
  ].join('\n');

  return new NextResponse(body, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
    },
  });
}

与 Astro 端点集成

在 Astro 中,创建一个 src/pages/llms.txt.ts 端点,带有 export const prerender = true 用于在构建时生成该文件。Astro 会在 构建期间调用 CMS API。 astro build 并将静态文件写入 dist/llms.txt:

src/pages/llms.txt.ts (Astro + Sanity)
// src/pages/llms.txt.ts
// Astro endpoint, fetch from CMS at build time (static generation)

import type { APIRoute } from 'astro';
import { createClient } from '@sanity/client';

// This endpoint is pre-rendered at build time
export const prerender = true;

const sanity = createClient({
  projectId: import.meta.env.SANITY_PROJECT_ID,
  dataset: import.meta.env.SANITY_DATASET || 'production',
  useCdn: false,
  apiVersion: '2024-01-01',
});

export const GET: APIRoute = async () => {
  const docs = await sanity.fetch(
    `*[_type == "doc" && !(_id in path("drafts.**"))] | order(order asc) {
      title, "slug": slug.current, summary
    }`
  );

  const SITE_URL = import.meta.env.SITE_URL || 'https://yoursite.com';

  const links = docs
    .map((doc: any) => `- [${doc.title}](${SITE_URL}/docs/${doc.slug}/): ${doc.summary ?? ''}`)
    .join('\n');

  const body = [
    '# Your Site',
    '',
    '> One-sentence description of your product.',
    '',
    '## Documentation',
    '',
    links,
  ].join('\n');

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

静态生成与动态生成

  • 静态(构建时),速度更快(通过 CDN 缓存)、更简单,且不依赖运行时 CMS。适合内容不常变化,或每次内容变化时都会部署的情况。
  • 动态(服务器路由),始终反映最新的 CMS 内容。最适合内容在两次部署之间频繁变化,或者内容变化时无法触发重新构建的情况。添加 Cache-Control 响应头,以免每次请求都对 CMS API 造成过大压力。

检查清单

  • 脚本或端点仅获取 已发布 内容(而非草稿)。
  • 所有生成的 URL 均为 绝对 (https://).
  • 文件开头恰好只有一个 H1。
  • blockquote 摘要紧跟在 H1 之后。
  • 每个章节至少包含一个链接。
  • 文件小于 20 KB(筛选内容,不要倾倒所有条目)。
  • 构建步骤在框架构建之前运行(prebuild 脚本或 CI 步骤)。
  • 已使用 llmtxt.info/validator/ 在每次部署后。

相关指南

来源