llms.txt для headless CMS
Headless CMS-платформы (Contentful, Sanity, Strapi, Directus) не обслуживают файлы напрямую: создавайте llms.txt во время сборки из данных CMS или получайте его во время запроса через серверный маршрут.
Последнее обновление:
Используете конструктор сайтов вместо headless CMS? См. специальные руководства для Shopify, Webflow, Wix и Squarespace.
Основной шаблон: API CMS → шаг сборки → статический файл
Headless CMS управляют контентом, но не отдают произвольные файлы по произвольным путям. Чтобы
опубликовать llms.txt, нужно получать содержимое из вашей CMS и самостоятельно
записывать файл либо в во время сборки (статический результат) или в время запроса (серверный маршрут).
- Запросите данные из CMS, получайте опубликованные страницы с полями title, slug и summary.
- Преобразуйте в ссылки Markdown, оформите каждую запись как
- [Title](https://url/): description. - Записать или вернуть файл, запишите в
public/llms.txtво время сборки или вернуть из серверного маршрута во время запроса.
Пример Contentful
Используйте JavaScript SDK Contentful
для получения записей через Content Delivery API. Запускайте этот скрипт на этапе сборки (например,
в package.json скриптах или вашем CI-конвейере) до сборки фреймворка:
// 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);
Добавьте скрипт в конвейер сборки:
{
"scripts": {
"prebuild": "node scripts/generate-llms-txt.mjs",
"build": "next build"
}
} Пример Sanity
Используйте GROQ, язык запросов Sanity, чтобы получать только необходимые поля. !(_id in path("drafts.**")) фильтр гарантирует включение только опубликованных документов:
// 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 предоставляет REST API по адресу /api/:collection. Используйте fields и filters параметры запроса, чтобы получать только опубликованные
документы с нужными вам полями:
// 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
Если вы хотите постоянно синхронизировать файл без полной пересборки, используйте обработчик
маршрута Next.js App Router с revalidate с нужным TTL. Next.js кэширует ответ и повторно
создаёт его в фоновом режиме:
// 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 вызовет API
CMS во время astro build и записать статический файл в dist/llms.txt:
// 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заголовок, чтобы не перегружать API CMS каждым запросом.
Контрольный список
- Скрипт или конечная точка загружает только опубликовано содержимое (не черновики).
- Все сгенерированные URL абсолютный (
https://). - Файл начинается ровно с одного H1.
- Сразу после H1 следует сводка в blockquote.
- В каждом разделе есть хотя бы одна ссылка.
- Файл меньше 20 КБ (отбирайте содержимое, не выгружайте каждую запись).
-
Шаг сборки выполняется перед сборкой фреймворка (
prebuildскрипт или шаг CI). - Проверено с помощью llmtxt.info/validator/ после каждого развёртывания.
Связанные руководства
- Как создать llms.txt, шаблоны и руководство по развёртыванию.
- Руководство по Next.js, App Router и статическая генерация.
- Руководство по Astro, коллекции контента и конечные точки.
- Справочник формата llms.txt, подробности спецификации.
- Валидатор · Генератор.