Agregar llms.txt al blog de Astro, guía práctica
Contexto
Administro IRC-Coding.de como desarrollador de aplicaciones, con más de 600 artículos sobre desarrollo de software y programación. Los motores de búsqueda de IA como ChatGPT, Perplexity y Claude rastrean sitios web e intentan entender el contenido. Sin embargo, luchan contra el marcado HTML, la renderización de JavaScript y el código innecesario.
La especificación llms.txt resuelve este problema: Un archivo de texto simple en el directorio raíz de un sitio que proporciona el contenido en un formato legible para máquinas.
Durante la implementación me planteé varias preguntas:
- ¿Cómo genero
llms.txtautomáticamente durante la compilación? - ¿Qué archivos necesito (
llms.txt,llms-full.txt,llms-small.txt)? - ¿Cómo protejo mi contenido contra copias simples por parte de estafadores?
Examiné dos enfoques conocidos: una ruta API personalizada (como Alex OP describe) y el plugin @4hse/astro-llms-txt (ver GitHub). En este artículo te muestro cómo comparé ambos y qué solución elegí para IRC-Coding.de.
Por cierto, el sitio de Alex OP es muy recomendable, déjate caer por allí. Lo encontré a través de una búsqueda en Google sobre llms.txt Astro.
Importante: incompatibilidad con Astro v7
⚠️ Atención desde Astro v7.0: El plugin @4hse/astro-llms-txt en versión 1.0.5 solo soporta hasta Astro v6 (peer astro@“^5.1.6 || ^6.0.0”). Con Astro v7, npm install falla con un error ERESOLVE. Esto afecta especialmente a despliegues en Railway, Vercel o Netlify que usan npm en lugar de pnpm.
Solución: Retira el plugin antes de actualizar a Astro v7 y genera los archivos llms.txt con un script propio. Existe un pull request para soporte de Astro v7 en el plugin competidor starlight-llms-txt, pero @4hse/astro-llms-txt aún no ha publicado una versión compatible.
Solución
Paso 1: instalar el plugin (hasta Astro v6)
npm install @4hse/astro-llms-txt
El plugin utiliza el hook astro:build:done de Astro, lee los archivos HTML generados del directorio dist/ y los convierte a Markdown con rehype-remark. Esto significa que no necesito implementar parsers propios ni lógica de extracción de contenido.
Paso 2: adaptar la configuración de Astro
En mi astro.config.mjs agregué el plugin a las integraciones:
import astroLlmsTxt from '@4hse/astro-llms-txt';
export default defineConfig({
site: 'https://www.deine-seite.de',
integrations: [
// ... otras integraciones
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',
}),
],
});
Paso 3: ejecutar la compilación
npm run build
Después de la compilación encontré tres archivos en dist/:
llms.txt: índice con título, descripción y enlaces a los DocSetsllms-full.txt: contenido de todas las páginas como Markdownllms-small.txt: solo estructura (encabezados y listas)
Paso 4: límite de extractos para protección de contenido
Por defecto, el plugin genera el contenido completo en llms-full.txt. Con mis 600 artículos, resultó un archivo de más de 140.000 líneas, un regalo para los ladrones de contenido. Cualquiera podía acceder a https://www.irc-coding.de/llms-full.txt y copiar el texto completo de todos los artículos.
Extendí el plugin para resolver esto. La opción excerptLength limita cada artículo a los primeros 600 caracteres. Al final aparece un enlace a la página original:
# Algoritmo explicado de forma sencilla
> Algoritmo explicado de forma comprensible: propiedades, paradigmas de diseño...
## Definición
Un algoritmo es una secuencia finita de instrucciones bien definidas...
[Visit full article](https://www.irc-coding.de/algorithmus-begriffserklaerung-komplexitaet-korrektheit)
Los motores de búsqueda de IA obtienen suficiente contexto para entender de qué se trata. El artículo completo solo existe en el sitio web. Para la adaptación, extendí la interfaz DocSet en el plugin y modifiqué la función buildEntryFromHtml para que corte el contenido en excerptLength caracteres y agregue el enlace de visita.
¿Por qué 600 caracteres? Probé algunos artículos y me di cuenta de que 600 caracteres son suficientes para que la IA entienda el contexto, pero insuficientes para copiar el artículo tal cual. Experimenta tú mismo, quizás 400 u 800 caracteres se adapten mejor a tu caso de uso.
Enfoques alternativos
Ruta API personalizada (Alex OP)
Alex OP describe un enfoque simple usando una ruta API de Astro:
// 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`;
// ... extracción de contenido
}
return new Response(llmsContent, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
};
Ventajas:
- Sin dependencias adicionales
- Control total sobre el formato
- Funciona también en el servidor de desarrollo
Desventajas:
- La extracción de contenido debe implementarse manualmente (eliminar componentes MDX, filtrar directivas Shiki-Twoslash, parsear frontmatter)
- No hay
llms-small.txtni DocSets estructurados - Sin modo
onlyStructure
Normalmente es una buena solución, pero revisa la página por tu cuenta, podría estar equivocado o malinterpretarlo.
Plugin @4hse/astro-llms-txt
El plugin de 4HSE adopta otro enfoque: lee los archivos HTML generados después del build y los convierte de vuelta a Markdown usando rehype-remark.
Ventajas:
- Pipeline completa (
rehype-parse→rehype-remark→remark-gfm→remark-stringify) - Las componentes MDX, Expressive Code y Tabs se procesan automáticamente
- Modo
onlyStructurepara una vista compacta - DocSets con patrones
include,promote,demotey coincidencia de patrones
Desventajas:
- Solo funciona durante el build, no en el servidor de desarrollo
- Sin límite de excerpts integrado (yo lo añadí por mi cuenta)
Comparativa
| Criterio | Custom API Route | @4hse/astro-llms-txt |
|---|---|---|
| Esfuerzo | Medio (lógica propia) | Bajo (configuración) |
| Soporte MDX | Implementación manual | Automático |
onlyStructure | No disponible | Integrado |
| Límite de excerpts | Implementación propia | Extensión necesaria |
| Servidor de desarrollo | Sí | Solo build |
| DocSets | No disponible | Con coincidencia de patrones |
Elegí el plugin @4hse/astro-llms-txt porque procesa automáticamente las componentes MDX y los bloques Expressive Code. Con más de 600 artículos, una extracción de contenido propia habría sido propensa a errores. El enfoque de Custom API Route de Alex OP es interesante para blogs pequeños, pero con mi volumen de artículos, el costo de mantenimiento de la extracción de contenido habría sido desproporcionado.
Solución a partir de Astro v7: Script propio
Dado que @4hse/astro-llms-txt aún no es compatible con Astro v7, ahora genero los archivos con un script Node propio. El script se ejecuta después del build de Astro y lee los archivos HTML de dist/, similar a como lo hacía el plugin.
La integración ocurre directamente en el script de build del package.json:
{
"scripts": {
"build": "astro build && node scripts/generate-llms-txt.mjs"
}
}
El script es compacto y sigue la especificación 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) {
// Elimina etiquetas script/style
let text = html.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, '');
// Elimina etiquetas HTML
text = text.replace(/<[^>]+>/g, ' ');
// Limpia espacios en blanco
text = text.replace(/\s+/g, ' ').trim();
return text;
}
function extractTitle(html) {
const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
return match ? match[1].trim() : 'Unbenannt';
}
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 (Index)
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);
Ventajas del script propio:
- Sin conflictos de peer-dependencies en upgrades de Astro
- Control total sobre la longitud y formato del excerpt
- Funciona en cualquier plataforma de deployment (Railway, Vercel, Netlify)
- Se adapta fácilmente a nuevos requisitos
Desventajas:
- Sin procesamiento automático de componentes MDX (solo extracción de texto)
- Sin modo
onlyStructurecon encabezados anidados - El mantenimiento está en tus manos
Para IRC-Coding.de con más de 300 artículos, este enfoque es suficiente. Los sistemas de IA obtienen el contexto, y mantengo la protección de contenido a través de excerptLength.
Errores comunes
-
Error:
Expected pattern to be a non-empty stringSolución: El plugin usapicomatchinternamente. Las cadenas vacías en los arrayspromoteoincludecausan este error. Usé['index', 'blog/**']en su lugar. -
Error:
File not found: dist/de/index.htmlSolución: El patróninclude: ['**']coincide con todas las páginas. Si algunas páginas no se generan (por ejemplo, condraft: true), aparece esta advertencia. Es inofensiva, el archivo simplemente se omite. -
Error:
llms-full.txtes demasiado grande Solución: EstablecíexcerptLengthen 600 caracteres. Esto protege mi contenido y mantiene el archivo compacto. Este ajuste requería una pequeña modificación del plugin ennode_modules/@4hse/astro-llms-txt/src/index.ts, donde extendí la interfazDocSetconexcerptLengthyvisitLinkText. -
Error: Los sistemas de IA no encuentran el archivo Solución: Asegúrate de que
llms.txtesté en la raíz de tu dominio (por ejemplo,https://www.irc-coding.de/llms.txt). En deployments en Vercel o Netlify, el archivo se sirve correctamente desdedist/automáticamente.
¿Detectan ChatGPT o Claude.ai tu llms.txt y vale la pena implementarlo?
Probablemente no, solo ayuda al que copia contenido.
Quedé sorprendido de lo rápido que se podía copiar mi sitio WordPress a Markdown, porque WordPress ya tenía todas las herramientas para hacerlo.
Aquí también es más bien un pensamiento comunitario, pero las IA generalmente se entrenan de otra forma.
Si le preguntas a ChatGPT, obtienes algo como esto:
Para mí como ChatGPT en este chat, un llms.txt no tiene una ventaja directa, porque no simplemente rastro cualquier sitio web en vivo y busco un llms.txt cuando respondo. Si no accedo a tu sitio web mediante búsqueda web o a través de un enlace, no veo el archivo en absoluto.
Pero si accedo a información de tu sitio mediante acceso web, entonces un llms.txt bien estructurado ciertamente puede ser útil, especialmente si:
- lista documentaciones importantes de manera clara,
- contiene buenos títulos y descripciones,
- apunta a páginas relevantes,
- e idealmente proporciona un llms-full.txt o contenido estructurado similar.
Para otros sistemas de IA
El potencial beneficio aquí es actualmente mayor. Sistemas y herramientas que soportan específicamente llms.txt pueden:
- encontrar documentación más rápidamente,
- priorizar contenido mejor,
- buscar menos páginas irrelevantes,
- y acceder más eficientemente a una documentación de API o proyecto.
¿Para SEO?
En el sentido clásico (ranking de Google) no hay beneficios comprobados actualmente.
Vería llms.txt más como:
- robots.txt → para crawlers,
- sitemap.xml → para motores de búsqueda,
- llms.txt → para sistemas impulsados por IA
Cumplen tareas diferentes.
No lo consideraría una medida de SEO, sino como una inversión en la detectabilidad y usabilidad de tu contenido para herramientas impulsadas por IA. Si llms.txt se establece más, ya estarás bien posicionado; si no, el trabajo adicional de una generación automática generalmente es manejable.
¿Por qué lo implementé de todos modos?
Bueno, el sitio me sirvió para aprender el material, y cada visitante adicional me hace feliz. Y la esperanza muere al último.
Preguntas frecuentes sobre llms.txt en Astro
1. ¿Qué es llms.txt?
2. ¿En qué se diferencia llms.txt de robots.txt?
3. ¿Necesito llms.txt para SEO?
4. ¿Qué archivos genera el plugin?
5. ¿Funciona el plugin en el servidor de desarrollo?
6. ¿Puedo crear llms.txt sin el plugin?
7. ¿Cómo protejo mi contenido contra copias?
8. ¿Qué significa onlyStructure?
9. ¿Cómo configuro promote y demote?
10. ¿Dónde debe estar llms.txt?
11. ¿Es llms.txt un estándar oficial?
12. ¿Puedo excluir subsecciones?
13. ¿Cuánto cuesta el plugin?
14. ¿Cuántos caracteres para excerptLength?
15. ¿Funciona llms.txt con SSG y SSR?
16. ¿Qué hago con Astro v7?
Referencias y fuentes
- Especificación llms.txt
- 4hse/astro-llms-txt en GitHub
- Alex OP: How I Added llms.txt to My Astro Blog
- Documentación de Astro


