Fredy Acuna
  • Publicaciones
  • Proyectos
  • Herramientas
  • Contacto
LinkedInXGitHubMedium

© 2026 Fredy Acuna

Volver a publicaciones
Cómo Agregar llms.txt a un Sitio con Next.js (Generado Desde Tu Contenido)

Cómo Agregar llms.txt a un Sitio con Next.js (Generado Desde Tu Contenido)

Fredy Acuna / October 30, 2026 / 6 min read

¿Qué Es llms.txt?

llms.txt es una propuesta para un archivo en la raíz de un sitio web, /llms.txt, que describe el sitio en markdown plano para modelos de lenguaje. Piénsalo como una tabla de contenido corta y curada: un título, un resumen de un párrafo y listas de enlaces con una frase sobre cada uno.

La motivación es práctica. Las páginas HTML están llenas de navegación, scripts y maquetación que un LLM tiene que atravesar, y la ventana de contexto es limitada. Un archivo markdown pequeño le da a un modelo (o a un agente de IA que navega en nombre de alguien) un mapa limpio de lo que contiene el sitio y dónde encontrarlo.

Una aclaración sobre las expectativas: llms.txt es una propuesta de la comunidad, no un estándar web como robots.txt o los sitemaps. Algunas herramientas y agentes de IA lo leen, pero no asumiría que un buscador o crawler en particular lo usa. Lo agregué porque es barato, es útil cuando le pides a un asistente que revise tu sitio y no cuesta nada mantenerlo si lo generas.


Lo Que Vas a Aprender

  • La estructura que sigue un archivo llms.txt
  • Cómo servirlo desde un route handler de Next.js
  • Cómo generarlo desde tu contenido MDX para que nunca quede desactualizado
  • Qué encabezados enviar

El Formato

La propuesta es simple. En orden:

  1. Un H1 con el nombre del sitio o proyecto (la única parte obligatoria)
  2. Un blockquote con un resumen corto
  3. Texto libre opcional
  4. Secciones H2 con listas de enlaces en markdown, cada uno opcionalmente seguido de : descripción
  5. Una sección opcional llamada ## Optional para enlaces que se pueden omitir cuando hay poco contexto

Un ejemplo mínimo:

# Fredy Acuna

> Personal portfolio and blog of Fredy Acuna, Software Engineer.

## Posts

- [Why Your Next.js Sitemap Is Empty](https://www.fredhii.com/posts/why-your-nextjs-sitemap-is-empty): next-sitemap fails when every page renders on request.

## Optional

- [RSS (EN)](https://www.fredhii.com/feed.xml)

Por Qué Generarlo en Lugar de Escribirlo

Podrías poner un archivo estático public/llms.txt en tu proyecto y listo. El problema es que empieza a desactualizarse el día que publicas tu siguiente artículo. Mi sitio tiene publicaciones y proyectos en dos idiomas, así que un archivo escrito a mano estaría mal en menos de una semana.

Next.js ya me da todo lo necesario para construirlo en cada petición: las mismas funciones getPosts() y getProjects() que usa el blog, y una configuración del sitio con la URL canónica y los enlaces sociales. Un route handler puede unir todo eso.


El Route Handler

En el App Router, una carpeta llamada llms.txt con un route.ts adentro sirve /llms.txt. El punto en el nombre de la carpeta no es problema.

app/
└── llms.txt/
    └── route.ts

Este es el handler completo de mi portafolio:

import { getPosts } from '@/lib/posts'
import { getProjects } from '@/lib/projects'
import { siteConfig } from '@/lib/config'

// llms.txt (https://llmstxt.org): a plain-markdown index of the site for LLMs.
export async function GET() {
  const [posts, projects] = await Promise.all([getPosts(), getProjects()])
  const { siteUrl, author, social } = siteConfig

  const link = (title: string | undefined, path: string, summary?: string) =>
    `- [${title || 'Untitled'}](${siteUrl}${path})${summary ? `: ${summary}` : ''}`

  const body = `# ${author.name}

> Personal portfolio and blog of ${author.name}, Software Engineer sharing insights on web development, technology, and projects. Content is published in English and Spanish.

Certifications: [AWS Certified Solutions Architect – Associate](${social.credly})

## Pages

- [Home](${siteUrl})
- [Posts (EN)](${siteUrl}/posts) / [Publicaciones (ES)](${siteUrl}/publicaciones)
- [Projects (EN)](${siteUrl}/projects) / [Proyectos (ES)](${siteUrl}/proyectos)
- [Tools (EN)](${siteUrl}/tools) / [Herramientas (ES)](${siteUrl}/herramientas)
- [Contact](${siteUrl}/contact)

## Posts

${posts.map(p => link(p.title, `/${p.locale === 'es' ? 'publicaciones' : 'posts'}/${p.slug}`, p.summary)).join('\n')}

## Projects

${projects.map(p => link(p.title, `/${p.locale === 'es' ? 'proyectos' : 'projects'}/${p.slug}`, p.summary)).join('\n')}

## Optional

- [RSS (EN)](${siteUrl}/feed.xml)
- [RSS (ES)](${siteUrl}/feed.es.xml)
- [GitHub](${social.github})
- [LinkedIn](${social.linkedin})
`

  return new Response(body, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Cache-Control': 's-maxage=3600, stale-while-revalidate'
    }
  })
}

Repasemos las decisiones.

Un Pequeño Helper Para Cada Enlace

El helper link genera un elemento de lista con la forma exacta que espera el formato: - [título](url): resumen. Cada publicación y proyecto ya tiene un summary en su frontmatter (los escribo de todos modos para SEO y redes sociales), así que cada entrada obtiene una descripción útil sin esfuerzo extra.

Cada Elemento Apunta a Su Propio Idioma

Las publicaciones en español viven bajo /publicaciones/ y las de inglés bajo /posts/. El handler elige el segmento según el locale de cada elemento, así que un modelo que sigue un enlace llega a una URL que responde 200 en el idioma correcto, no a una redirección.

URLs Absolutas Desde un Solo Lugar

Todos los enlaces se construyen con siteConfig.siteUrl, el mismo valor que usan el sitemap, el robots y las etiquetas canonical. Si el host canónico cambia, todos cambian juntos. (Lo aprendí por las malas cuando mi sitemap apuntaba a un host que redirigía; esa historia está en Por Qué Tu Sitemap de Next.js Está Vacío.)

Qué Incluye

  • Las páginas principales, con sus versiones en inglés y español lado a lado
  • Cada publicación, con su resumen
  • Cada proyecto, con su resumen
  • Mi certificación, como enlace a la insignia verificable
  • En ## Optional: los dos feeds RSS y mis perfiles sociales

La sección Optional está pensada para lo que un modelo puede omitir cuando tiene poco contexto. Los feeds y los perfiles sociales encajan bien ahí.

Encabezados

  • Content-Type: text/plain; charset=utf-8: el archivo es texto en markdown. El charset importa porque los títulos y resúmenes tienen tildes y caracteres como la raya.
  • Cache-Control: s-maxage=3600, stale-while-revalidate: la CDN puede guardarlo en caché una hora y servir la copia guardada mientras se actualiza en segundo plano. El contenido solo cambia cuando despliego un artículo nuevo, así que no hay razón para reconstruirlo en cada petición.

Probarlo

Levanta el servidor de desarrollo y pídelo:

curl -i http://localhost:3000/llms.txt

Verifica que:

  • El estado sea 200 y el Content-Type sea text/plain; charset=utf-8
  • La publicación más reciente aparezca bajo ## Posts
  • Los enlaces usen tu host canónico (con o sin www, el que responda 200)

Puntos Clave

  • llms.txt es un índice corto en markdown de tu sitio para LLMs, propuesto en llmstxt.org; trátalo como un extra útil, no como un requisito de SEO
  • Un route handler en app/llms.txt/route.ts lo sirve y puede reutilizar las mismas funciones de contenido que tu blog
  • Generarlo desde el contenido hace que se actualice con cada despliegue y nunca quede desactualizado
  • Construye cada URL con la misma URL canónica que usan tu sitemap y tus etiquetas canonical
  • Sírvelo como text/plain con charset UTF-8 y una caché razonable en la CDN

Suscribite al newsletter

Te aviso cuando publico algo nuevo.

Cuidamos tus datos. Leé nuestra política de privacidad.