Добавляем llms.txt на блог на Astro, пошаговое руководство
Исходная ситуация
Я разработчик приложений и веду IRC-Coding.de с более чем 600 статьями о разработке программного обеспечения и программировании. AI-поисковые системы вроде ChatGPT, Perplexity и Claude сканируют веб-сайты и пытаются понять их содержимое. При этом они борются с HTML-разметкой, JavaScript-рендерингом и лишним кодом.
Спецификация llms.txt решает эту проблему: простой текстовый файл в корневой директории сайта, который предоставляет содержимое в машиночитаемом формате.
При внедрении я столкнулся со следующими вопросами:
- Как генерировать
llms.txtавтоматически при сборке? - Какие файлы мне нужны (
llms.txt,llms-full.txt,llms-small.txt)? - Как защитить мой контент от простого копирования мошенниками?
Я изучил два популярных подхода: custom API Route (как описывает Alex OP) и плагин @4hse/astro-llms-txt (см. GitHub). В этой статье я расскажу, как я оба сравнил и какое решение выбрал для IRC-Coding.de.
Кстати, сайт Alex OP действительно стоит посмотреть. Я нашел его через поиск Google по запросу llms.txt Astro.
Важно: несовместимость с Astro v7
⚠️ Внимание при обновлении на Astro v7.0: Плагин @4hse/astro-llms-txt версии 1.0.5 поддерживает только до Astro v6 (peer astro@“^5.1.6 || ^6.0.0”). При Astro v7 установка через npm install завершается с ошибкой ERESOLVE. Это особенно актуально для развертывания на Railway, Vercel или Netlify, которые используют npm вместо pnpm.
Решение: Удалите плагин перед обновлением на Astro v7 и генерируйте файлы llms.txt собственным скриптом. Pull request для поддержки Astro v7 существует в конкурирующем плагине starlight-llms-txt, но @4hse/astro-llms-txt еще не выпустил совместимый релиз.
Решение
Шаг 1: установка плагина (до Astro v6)
npm install @4hse/astro-llms-txt
Плагин использует хук Astro astro:build:done, читает сгенерированные HTML-файлы из директории dist/ и преобразует их в Markdown с помощью rehype-remark. Это означает: мне не нужно писать собственные парсеры или логику извлечения контента.
Шаг 2: настройка конфигурации Astro
В своем astro.config.mjs я добавил плагин к интеграциям:
import astroLlmsTxt from '@4hse/astro-llms-txt';
export default defineConfig({
site: 'https://www.deine-seite.de',
integrations: [
// ... другие интеграции
astroLlmsTxt({
title: 'IRC-Coding',
description: 'Software Development and Programming, Tutorials, Artikel und Ressourcen.',
notes: '- Content is auto-generated from the official source at https://www.irc-coding.de',
docSet: [
{
title: 'Complete site',
description: 'Excerpts of all blog articles with links to full content',
url: '/llms-full.txt',
include: ['**'],
promote: ['index', 'blog/**'],
excerptLength: 600,
visitLinkText: 'Visit full article',
},
{
title: 'Compact overview',
description: 'Structure-only index of all pages',
url: '/llms-small.txt',
include: ['**'],
onlyStructure: true,
promote: ['index', 'blog/**'],
},
],
pageSeparator: '\n\n---\n\n',
}),
],
});
Шаг 3: запуск сборки
npm run build
После сборки я нашел три файла в dist/:
llms.txt: индекс с названием, описанием и ссылками на наборы документовllms-full.txt: содержимое всех страниц в формате Markdownllms-small.txt: только структура (заголовки и списки)
Шаг 4: ограничение отрывков для защиты контента
По умолчанию плагин генерирует полное содержимое в llms-full.txt. При моих 600 статьях получился файл с более чем 140.000 строк, что является настоящим подарком для контент-воров. Каждый мог открыть https://www.irc-coding.de/llms-full.txt и скопировать весь текст всех статей.
Я расширил плагин. Опция excerptLength ограничивает каждую статью первыми 600 символами. В конце появляется ссылка на оригинальную страницу:
# Algorithmus einfach erklärt
> Algorithmus verständlich erklärt: Eigenschaften, Entwurfsparadigmen...
## Definition
Ein Algorithmus ist eine endliche Folge von wohldefinierten Anweisungen...
[Visit full article](https://www.irc-coding.de/algorithmus-begriffserklaerung-komplexitaet-korrektheit)
AI-поисковые системы получают достаточно контекста, чтобы понять, о чем идет речь. Полная статья доступна только на самом сайте. Для этого я расширил интерфейс DocSet в плагине и модифицировал функцию buildEntryFromHtml так, чтобы она обрезала контент на excerptLength символах и добавляла ссылку для посещения.
Почему 600 символов? Я протестировал несколько статей и заметил: 600 символов достаточно, чтобы AI понял контекст, но недостаточно для простого копирования статьи 1:1. Попробуйте сами, может быть для вашего случая подойдут и 400, или 800.
Альтернативные подходы
Custom API Route (Alex OP)
Alex OP описывает простой подход с использованием Astro API Route:
// src/pages/llms.txt.ts
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
export const GET: APIRoute = async () => {
const posts = await getCollection("blog", ({ data }) => !data.draft);
const sortedPosts = posts.sort(
(a, b) => new Date(b.data.pubDatetime).valueOf() - new Date(a.data.pubDatetime).valueOf()
);
let llmsContent = "";
for (const post of sortedPosts) {
llmsContent += `---\ntitle: ${post.data.title}\ndescription: ${post.data.description}\n---\n\n`;
// ... извлечение контента
}
return new Response(llmsContent, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
};
Преимущества:
- Без дополнительных зависимостей
- Полный контроль над форматированием
- Работает даже на dev-сервере
Недостатки:
- Извлечение контента нужно реализовать самому (удалять MDX-компоненты, фильтровать директивы Shiki-Twoslash, парсить frontmatter)
- Нет
llms-small.txtи структурированных наборов документов - Нет режима
onlyStructure
Обычно это хороший подход, но посмотрите на сайт сами, я могу ошибаться или неправильно что-то понять.
Плагин @4hse/astro-llms-txt
Этот плагин от 4HSE работает иначе. Он читает готовые HTML-файлы после сборки и преобразует их обратно в Markdown с помощью rehype-remark.
Плюсы:
- Полный пайплайн (
rehype-parse→rehype-remark→remark-gfm→remark-stringify) - MDX-компоненты, Expressive Code и табы обрабатываются автоматически
- Режим
onlyStructureдля компактного представления - DocSets с паттерн-матчингом
include,promote,demote
Минусы:
- Работает только при сборке (не на dev-сервере)
- Нет встроенного лимита на размер выписки (я добавил его самостоятельно)
Сравнение
| Критерий | Custom API Route | @4hse/astro-llms-txt |
|---|---|---|
| Усилия | Среднее (своя логика) | Минимальное (конфиг) |
| Поддержка MDX | Нужно реализовать вручную | Автоматическая |
onlyStructure | Отсутствует | Встроен |
| Лимит выписки | Реализовать самому | Расширить самому |
| Dev-сервер | Да | Только сборка |
| DocSets | Отсутствуют | С паттерн-матчингом |
Я выбрал @4hse/astro-llms-txt, потому что он автоматически обрабатывает MDX-компоненты и блоки Expressive Code. С более чем 600 статей собственная экстракция контента была бы слишком подвержена ошибкам. Подход Alex OP с custom API route интересен для небольших блогов, но при таком количестве статей затраты на поддержку экстракции контента стали бы непомерными.
Решение для Astro v7: собственный скрипт
Поскольку @4hse/astro-llms-txt пока несовместим с Astro v7, я генерирую файлы с помощью собственного Node-скрипта. Он запускается после сборки Astro и читает HTML-файлы из dist/, как это делал плагин.
Интеграция прямо в package.json:
{
"scripts": {
"build": "astro build && node scripts/generate-llms-txt.mjs"
}
}
Сам скрипт компактен и следует спецификации llms.txt:
// scripts/generate-llms-txt.mjs
import { readdir, readFile, writeFile, stat } from 'node:fs/promises';
import { join } from 'node:path';
const SITE = 'https://www.irc-coding.de';
const DIST = './dist';
const EXCERPT_LENGTH = 600;
async function findHtmlFiles(dir, base = '') {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const relPath = base ? `${base}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
files.push(...await findHtmlFiles(fullPath, relPath));
} else if (entry.name === 'index.html') {
files.push({ fullPath, relPath: base || '' });
}
}
return files;
}
function extractText(html) {
// Удаляем теги script и style
let text = html.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, '');
// Удаляем HTML-теги
text = text.replace(/<[^>]+>/g, ' ');
// Очищаем пробелы
text = text.replace(/\s+/g, ' ').trim();
return text;
}
function extractTitle(html) {
const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
return match ? match[1].trim() : 'Без названия';
}
async function main() {
const files = await findHtmlFiles(DIST);
const pages = [];
for (const file of files) {
if (file.relPath.startsWith('404') || file.relPath.startsWith('admin')) continue;
const html = await readFile(file.fullPath, 'utf-8');
const title = extractTitle(html);
const text = extractText(html);
const excerpt = text.slice(0, EXCERPT_LENGTH);
const url = file.relPath ? `${SITE}/${file.relPath}/` : `${SITE}/`;
pages.push({ title, excerpt, url, relPath: file.relPath });
}
// llms.txt (индекс)
let llmsTxt = `# ${SITE}\n\n> Software Development and Programming\n\n`;
llmsTxt += `## Docs\n\n- [Complete site](${SITE}/llms-full.txt): Excerpts of all articles\n- [Compact overview](${SITE}/llms-small.txt): Structure-only index\n`;
await writeFile(join(DIST, 'llms.txt'), llmsTxt, 'utf-8');
// llms-full.txt
let llmsFull = `# ${SITE}\n\n> Software Development and Programming\n\n`;
for (const page of pages) {
llmsFull += `\n---\n\n## ${page.title}\n\n${page.excerpt}\n\n[Visit full article](${page.url})\n`;
}
await writeFile(join(DIST, 'llms-full.txt'), llmsFull, 'utf-8');
// llms-small.txt
let llmsSmall = `# ${SITE}\n\n> Software Development and Programming\n\n`;
for (const page of pages) {
llmsSmall += `- [${page.title}](${page.url})\n`;
}
await writeFile(join(DIST, 'llms-small.txt'), llmsSmall, 'utf-8');
console.log(`Generated llms.txt, llms-full.txt, llms-small.txt with ${pages.length} pages`);
}
main().catch(console.error);
Плюсы собственного скрипта:
- Нет конфликтов peer-зависимостей при обновлении Astro
- Полный контроль над длиной выписки и форматом
- Работает на любой платформе деплоя (Railway, Vercel, Netlify)
- Легко адаптировать под новые требования
Минусы:
- Нет автоматической обработки MDX-компонентов (только текст)
- Нет режима
onlyStructureс вложенными заголовками - Поддержка лежит на тебе
Для IRC-Coding.de с более чем 300 статей этого подхода достаточно. KI-системы получают контекст, а защита контента через excerptLength остаётся на месте.
Частые ошибки
-
Ошибка:
Expected pattern to be a non-empty stringРешение: Плагин используетpicomatch. Пустые строки в массивахpromoteилиincludeвызывают эту ошибку. Вместо этого я использовал['index', 'blog/**']. -
Ошибка:
File not found: dist/de/index.htmlРешение: Паттернinclude: ['**']совпадает со всеми страницами. Если какие-то страницы не генерируются (например, черезdraft: true), появляется это предупреждение. Оно безобидно, файл просто пропускается. -
Ошибка:
llms-full.txtслишком большой Решение: Я установилexcerptLengthна 600 символов. Это защищает мой контент и держит файл компактным. Для этого потребовалось небольшое изменение плагина вnode_modules/@4hse/astro-llms-txt/src/index.ts, где я расширил интерфейсDocSetсвойствамиexcerptLengthиvisitLinkText. -
Ошибка: KI-системы не находят файл Решение: Убедись, что
llms.txtнаходится в корне вашего домена (например,https://www.irc-coding.de/llms.txt). При деплоях на Vercel или Netlify файл изdist/автоматически доставляется корректно.
Действительно ли ChatGPT или Claude.ai учитывают твой llms.txt и стоит ли тратить на это время?
Вероятно, нет. Это скорее помогает тем, кто копирует контент.
Я был шокирован, насколько быстро можно скопировать WordPress-сайт в Markdown, потому что WordPress уже содержит все необходимые инструменты.
Здесь это скорее движение сообщества, но KI-системы обучаются иначе.
Когда я спросил ChatGPT, вот что он ответил:
Для меня как ChatGPT в этом чате llms.txt не дает прямых преимуществ, потому что я не просто краулю произвольные сайты в реальном времени и не ищу llms.txt при ответе. Если ты не вызываешь поиск в вебе или не предоставляешь мне ссылку, я файл вообще не вижу.
Но если я получаю информацию с твоего сайта через веб-доступ, хорошо структурированный llms.txt действительно может помочь, особенно если он:
содержит важную документацию в понятном виде,
имеет хорошие заголовки и описания,
ссылается на релевантные страницы,
и предоставляет структурированный контент типа llms-full.txt.
Для других KI-систем
Потенциальная польза здесь больше. Системы и инструменты, которые специально поддерживают llms.txt, могут:
быстрее находить документацию,
лучше расставлять приоритеты контента,
меньше искать нерелевантные страницы,
и эффективнее использовать API или документацию проекта.
Для SEO?
В классическом смысле (ранжирование в Google) пока нет подтвержденных преимуществ.
Я бы рассматривал llms.txt скорее так:
robots.txt → для краулеров,
sitemap.xml → для поисковых машин,
llms.txt → для KI-систем.
Они выполняют разные задачи.
Но я не считаю это мерой SEO, а скорее инвестицией в то, чтобы твой контент был проще найти и использовать KI-инструментам. Если llms.txt получит широкое распространение, ты уже будешь готов. Если нет, дополнительные затраты при автоматизированной генерации обычно минимальны.
Почему я всё равно это внедрил?
Сайт служит мне для обучения, и каждый новый посетитель радует меня. И надежда умирает последней.
FAQ llms.txt для Astro
1. Что такое llms.txt?
2. Чем llms.txt отличается от robots.txt?
3. Нужна ли мне llms.txt для SEO?
4. Какие файлы генерирует плагин?
5. Работает ли плагин на dev-сервере?
6. Могу ли я создать llms.txt без плагина?
7. Как защитить контент от копирования?
8. Что означает onlyStructure?
9. Как настроить promote и demote?
10. Где должна находиться llms.txt?
11. Является ли llms.txt официальным стандартом?
12. Могу ли я исключить некоторые подстраницы?
13. Сколько стоит плагин?
14. Сколько символов использовать для excerptLength?
15. Работает ли llms.txt с SSG и SSR?
16. Что делать с Astro v7?
Источники статьи
- Спецификация llms.txt
- 4hse/astro-llms-txt на GitHub
- Alex OP: How I Added llms.txt to My Astro Blog
- Документация Astro


