Módulo F1 de 5 · 40 minutos de lectura
F1. Que puedan leerte
Fundamentos de accesibilidad generativa
F1 es el módulo 1 de los 5 del curso GEO de esGEO, gratis y completo en esta página. Si tu HTML llega vacío al crawler, nada de lo demás cuenta. Cómo comprobarlo y cómo arreglarlo, con o sin tocar tu framework. Está pensado para quien tiene una web propia o lleva las de sus clientes, y son unas 2 horas entre leer y aplicar. Edición de octubre de 2026.
Antes de empezar
Este módulo enseña a comprobar y a arreglar que los rastreadores de IA puedan leer tu web: que el texto llegue en el HTML que sirve tu servidor, que robots.txt les deje pasar y que sitemap.xml y llms.txt les digan qué hay. Está pensado para quien tiene una web propia o lleva las de sus clientes, esté hecha con un CMS, con un constructor visual o con un framework de JavaScript. Son unas 2 horas entre leer y aplicar. Edición de octubre de 2026.
F1 va primero por una razón sencilla: si una máquina no puede leer tu página, nada de lo que veas en los módulos siguientes cuenta. Al terminar sabrás comprobar en cinco minutos si tu web sirve el texto o un contenedor vacío, arreglarlo según tu herramienta y publicar un robots.txt, un sitemap.xml y un llms.txt correctos. Es lo que hace falta para pasar los dos gates de HABLA (H y A).
Qué necesitas
- Un terminal con curl y perl, que vienen instalados en macOS y en la mayoría de distribuciones de Linux. En Windows, usa WSL.
- Python 3, para una comprobación opcional del capítulo 4.
- Acceso a tu web: al panel de tu CMS o constructor, o al código y al despliegue.
- HABLA, el auditor gratuito de esGEO: https://www.esgeo.ai/geo-score (gratis y sin registro).
Todos los comandos de este módulo se ejecutaron contra https://www.esgeo.ai el 5 de octubre de 2026, y las salidas que ves son las reales de ese día. Cuando los ejecutes tú, cambia la URL por la tuya.
Apunta tu nota de partida
Antes de tocar nada, audita en HABLA tu página principal y tus dos o tres páginas más importantes. Apunta de cada una la nota, la fecha y si pasa el gate A: es tu línea de base. Cuando apliques F1 y F2, vuelve a auditarlas con la misma herramienta y compara.
Clave: La nota de HABLA cambia en cuanto publicas los cambios. Que ChatGPT o Perplexity te citen depende de cuándo vuelvan a rastrear tu web y de tu autoridad: nadie puede garantizar un plazo.
Hazlo hoy: audita tus páginas clave en HABLA y guarda la nota, la fecha y el estado del gate A de cada una.
1. Cómo llega una IA a tu web
Un asistente como ChatGPT, Claude, Perplexity o Gemini no visita tu web como una persona. Un programa, el rastreador o bot, pide una URL a tu servidor, recibe una respuesta y se queda con el texto. Lo que no llega en esa respuesta no existe para él.
1.1 Del rastreo a la cita
- Rastreo. Un bot pide tu página y descarga lo que devuelve tu servidor: el HTML inicial.
- Índice. El texto se guarda y se trocea, para entrenar modelos o para un índice de búsqueda que se consulta al responder.
- Recuperación. Cuando alguien pregunta, el sistema busca en su índice, o en la web en ese momento, los fragmentos que mejor responden, y el modelo redacta apoyándose en ellos. Es lo que se llama RAG (retrieval-augmented generation, generación aumentada por recuperación).
- Cita. Si el sistema enseña fuentes, enlaza los documentos de los que ha sacado la respuesta. Ahí aparece tu URL, o la de otro.
Entrenamiento y recuperación son caminos distintos. Un modelo entrenado con tu texto puede «saber» cosas de tu negocio, pero eso no es una cita: las fuentes enlazadas de la búsqueda de ChatGPT o de Perplexity son documentos recuperados para esa respuesta. Por eso las empresas de IA usan bots distintos y casi todas lo documentan: unos recogen texto para entrenar (GPTBot, ClaudeBot), otros construyen el índice de búsqueda (OAI-SearchBot, Claude-SearchBot, PerplexityBot) y otros descargan una página cuando un usuario lo pide (ChatGPT-User, Claude-User, Perplexity-User). Todos empiezan igual: pidiendo tu HTML.
1.2 Por qué el HTML inicial lo decide todo
Cuando tu navegador abre una aplicación de una sola página (una SPA: React, Vue o Angular sin renderizado en servidor), recibe primero un HTML casi vacío, con un contenedor como <div id="root"></div> y varios scripts. Después ejecuta el JavaScript, que pinta el contenido. Tú ves la página completa; un bot que no ejecuta JavaScript se queda con el contenedor vacío.
¿Qué bots no ejecutan JavaScript? Lo que se ha medido y documentado:
- Un análisis publicado por Vercel en diciembre de 2024 concluyó que «none of the major AI crawlers currently render JavaScript». Lo comprobaron con los rastreadores de OpenAI (OAI-SearchBot, ChatGPT-User y GPTBot), Anthropic (ClaudeBot), Meta, ByteDance y Perplexity: descargan los ficheros .js como texto, pero no los ejecutan.
- Common Crawl, el archivo abierto de la web que rastrea CCBot, lo dice en sus preguntas frecuentes: «Currently, JavaScript is not executed».
- Hay excepciones. Según el mismo análisis, Gemini usa la infraestructura de Googlebot, que sí renderiza JavaScript, y Applebot también lo hace; Apple lo confirma: Applebot «may render the content of your website within a browser».
No dependas de que el bot ejecute tu JavaScript: los rastreadores de OpenAI, Anthropic y Perplexity que se han medido no lo hacen. Y como esto puede cambiar, no te fíes de lo que lees, tampoco de este módulo: compruébalo con curl, como harás en el capítulo 2.
Clave: El texto que importa tiene que estar en el HTML que devuelve tu servidor en la primera respuesta. No después de un clic ni después de ejecutar JavaScript.
1.3 Cuántas webs tienen el problema
No es la mayoría. En el estudio de esGEO de octubre de 2026 analizamos la portada de 135 webs de grandes empresas españolas (de 179 que intentamos auditar): 16, el 11,9 %, servían el HTML vacío. Para esas webs, el resto del trabajo no sirve de nada hasta que lo arreglen. Y el problema aparece también en páginas sueltas de webs que en general están bien: un configurador, una sección hecha con un widget, unas preguntas frecuentes que se cargan al hacer clic. El mismo estudio mide los otros dos frentes del módulo: solo el 14,8 % daba alguna directiva a bots de IA en su robots.txt, y el 31,1 % publicaba un llms.txt.
HABLA resume el límite de este trabajo en el aviso que acompaña cada nota: «HABLA mide si una máquina PUEDE leerte y extraer una afirmación citable. No mide autoridad». F1 quita la barrera técnica, pero que te elijan como fuente depende también de lo que dices (F2), de tu autoridad (F3) y de tus datos estructurados (F4). En el paper «GEO: Generative Engine Optimization» (Aggarwal et al., KDD 2024), citar fuentes y añadir citas y estadísticas aumentó la visibilidad hasta un 40 % en su benchmark. Son mejoras del contenido; para que cuenten, tiene que poder leerse.
Hazlo hoy: apunta con qué está hecha tu web (CMS, constructor o framework) y quién puede tocar su configuración y su despliegue. Lo necesitarás en el capítulo 3.
2. La prueba del HTML vacío
Vas a ver tu página como la ve un bot que no ejecuta JavaScript: primero con el navegador y después con curl, que hace lo mismo que un rastreador.
2.1 Ver código fuente frente a inspeccionar
| Vista | Cómo se abre | Qué enseña |
|---|---|---|
| Ver código fuente | Ctrl+U en Windows y Linux; Cmd+Opción+U en Mac | El HTML tal como lo envió el servidor: lo que recibe el bot |
| Inspeccionar | Clic derecho → Inspeccionar | El DOM después de ejecutar JavaScript: lo que ves tú |
Abre «ver código fuente» (en Safari, activa antes el menú Desarrollo) y busca una frase de la mitad de la página, no del menú. Si no aparece ahí pero sí en «inspeccionar», ese texto lo pinta JavaScript y un bot que no lo ejecuta no lo verá. Es una prueba manual que no mide cuánto texto llega; para eso, curl.
2.2 La prueba con curl
Descarga la página con el user-agent de GPTBot que publica OpenAI (consultado el 5 de octubre de 2026), porque algunas webs responden distinto según quién pregunte. La variable va en dos líneas para que quepa en la página.
UA='Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible;'
UA="$UA GPTBot/1.4; +https://openai.com/gptbot"
curl -s -A "$UA" -o pagina.html \
-w 'HTTP %{http_code} · %{size_download} bytes\n' https://www.esgeo.ai/
Salida (5 de octubre de 2026):
HTTP 200 · 94318 bytes
Un 403 significaría que el servidor o tu CDN rechazan a ese bot (capítulo 4). El tamaño no dice nada, porque una página vacía puede pesar mucho por sus scripts. Lo que importa es el texto. Para medirlo, guarda el guion de abajo: descarga la página como GPTBot, busca un contenedor raíz vacío (un <div> con id root, app, __next o __nuxt, los nombres que usan React, Vue, Next.js y Nuxt), saca los h1 y cuenta el texto de contenido igual que HABLA: quita scripts, estilos y comentarios, se queda con <main> (o <article>) si existe y quita menú, cabecera, pie y barras laterales.
cat > prueba-html.sh <<'EOF'
#!/usr/bin/env bash
# prueba-html.sh: lo que lee un rastreador que no ejecuta JavaScript
# Uso: bash prueba-html.sh https://www.tudominio.es/pagina
UA='Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible;'
UA="$UA GPTBot/1.4; +https://openai.com/gptbot"
curl -s -A "$UA" -o pagina.html \
-w 'HTTP %{http_code} · %{size_download} bytes\n' "$1"
perl -CSD -Mutf8 -0777 -ne '
my $vacio = /<div[^>]+id=["\x27](root|app|__next|__nuxt)["\x27][^>]*>\s*<\/div>/;
my @h1;
while (/<h1\b[^>]*>(.*?)<\/h1>/gis) {
(my $t = $1) =~ s/<[^>]+>| |&\w+;/ /g; $t =~ s/\s+/ /g; $t =~ s/^ | $//g;
push @h1, $t;
}
s/<script\b.*?<\/script>|<style\b.*?<\/style>|<!--.*?-->/ /gis;
$_ = $1 if /(<main\b.*?<\/main>)/is || /(<article\b.*?<\/article>)/is;
s/<(nav|header|footer|aside)\b.*?<\/\1>/ /gis;
s/<[^>]+>/ /g; s/ |&\w+;/ /g; s/\s+/ /g; s/^ | $//g;
my $n = length;
open my $f, ">:utf8", "texto.txt"; print $f "$_\n"; close $f;
print "Contenido: $n caracteres (guardado en texto.txt)\n";
print "Contenedor raíz vacío: ", ($vacio ? "sí" : "no"), "\n";
print "h1: ", (@h1 ? join(" | ", @h1) : "ninguno"), "\n";
print "Gate A de HABLA: ", ($vacio || $n < 400 ? "NO pasa" : "pasa"), "\n";
' pagina.html
EOF
bash prueba-html.sh https://www.esgeo.ai/
Salida:
HTTP 200 · 94318 bytes
Contenido: 11080 caracteres (guardado en texto.txt)
Contenedor raíz vacío: no
h1: Haz que la IA recomiende tu negocio
Gate A de HABLA: pasa
Los 11.080 caracteres son la misma cifra que obtuvo la rúbrica de HABLA (su comprobación content_chars) sobre esa página ese día.
Compara con una página vacía de verdad: la de pago completado de esgeo.ai (/success) sigue siendo SPA a propósito, porque no tiene nada que citar, y sirve el mismo tipo de shell que toda la web antes del arreglo del capítulo 6.
bash prueba-html.sh https://www.esgeo.ai/success
Salida:
HTTP 200 · 5238 bytes
Contenido: 127 caracteres (guardado en texto.txt)
Contenedor raíz vacío: sí
h1: ninguno
Gate A de HABLA: NO pasa
Responde 200, igual que la portada: el código de estado no te avisa de nada. Para buscar una frase concreta, búscala en texto.txt (aquí, tras volver a pasar el guion por la portada) y no en pagina.html:
bash prueba-html.sh https://www.esgeo.ai/ > /dev/null
grep -c "no ejecutan JavaScript" texto.txt
Salida:
1
Muchos frameworks meten el contenido también dentro de un <script> con los datos para hidratar la página, y en el HTML crudo puedes encontrar una frase que no está en el texto que lee el bot. Lo comprobamos con Next.js 16: un texto que solo se pinta tras montarse en el navegador aparecía en el HTML crudo, dentro de un script, y no en el texto. HABLA quita los scripts antes de contar; haz tú lo mismo.
2.3 Qué es un shell para HABLA
HABLA, con su rúbrica 2.0, considera que una página es un shell si tiene un contenedor raíz vacío o si su texto de contenido (el que cuenta el guion: sin menú, cabecera, pie ni barras laterales) tiene menos de 300 caracteres. Un menú muy largo no te salva. La dimensión A (Accesible) se puntúa así:
| Texto de contenido | Nota de A | Gate A |
|---|---|---|
| Shell (raíz vacía o menos de 300 caracteres) | 15 | No pasa |
| De 300 a 399 caracteres | 20 | No pasa |
| De 400 a 1.199 caracteres | 55 | Pasa |
| De 1.200 a 2.999 caracteres | 80 | Pasa |
| 3.000 caracteres o más | 100 | Pasa |
Y es un gate: si A no pasa, o si H (la higiene) queda por debajo de 50, la nota total no puede pasar de 39, la franja MUDA. El 5 de octubre de 2026, la rúbrica de HABLA daba a /success H 85 y X 100, porque comparte robots.txt, sitemap y llms.txt con el resto del dominio, pero A 15, B 45 y L 12. La suma ponderada salía 43 y el gate la dejó en 39. La portada, ese día: 95, BILINGÜE.
Aviso: HABLA busca el contenedor vacío en todo el HTML. Si tu página tiene texto pero también un
<div id="app"></div>vacío (por ejemplo, el hueco donde se monta un widget), la marcará como shell. Cambia el id de ese contenedor o haz que llegue con contenido.
2.4 Tres trampas al hacer la prueba
- Probar solo la portada. Prueba también las páginas que quieres que te citen: servicios, fichas, precios, artículos.
- Contenido que existe, pero no se envía. Pestañas, acordeones y botones de «ver más» pueden no estar en el HTML hasta que alguien hace clic. En esgeo.ai, el acordeón de preguntas frecuentes no incluía las respuestas cerradas; se arregló obligándolo a renderizarlas (en la librería Radix, con la opción
forceMount). - Servir a los bots una versión distinta según su user-agent (dynamic rendering). Google lo describe como «a workaround and not a long-term solution» y recomienda renderizar en servidor, el renderizado estático o la hidratación.
Hazlo hoy: pasa prueba-html.sh por tus tres páginas más importantes y apunta, para cada una, los caracteres de contenido, el h1 y si pasa el gate A.
3. Cómo arreglarlo según tu stack
Hay tres formas de que el texto viaje en la respuesta del servidor: renderizar en servidor en cada petición (SSR), generar el HTML completo en el build (prerender o SSG) o publicar HTML con el contenido dentro, como hacen los CMS y los constructores. Se comprueba siempre igual: bash prueba-html.sh con tu URL y grep -c con una frase sobre texto.txt. Si el texto aparece, está arreglado, diga lo que diga la documentación de tu herramienta.
3.1 WordPress
WordPress genera el HTML en el servidor: según su documentación, carga una plantilla y «the server then parses the code in the template and returns HTML to the visitor». Lo que suele fallar es lo que le añades: plugins que cargan contenido con JavaScript (pestañas, sliders, reseñas, «cargar más») y contenido incrustado con un iframe o un script de otro servicio. Si usas WordPress headless y otra aplicación pinta la web, aplica la sección de esa herramienta. Pasa el guion por una página de cada plantilla y busca en texto.txt una frase de cada bloque que te preocupe; si no aparece, busca en el plugin una opción de renderizado en servidor o cámbialo por un bloque nativo.
3.2 Shopify
Los temas de Shopify usan Liquid, y su guía de rendimiento pide «Render initial content with Liquid and HTML instead of client-side frameworks» para títulos, precios, descripciones, colecciones y navegación. Lo que suele fallar son las aplicaciones que añaden su bloque con JavaScript (reseñas, preguntas frecuentes, guías de tallas). Hydrogen, el framework headless de Shopify, crea aplicaciones de React Router que, según su documentación, renderizan en servidor por defecto. Pasa el guion por una ficha y una colección, y busca en texto.txt una frase de la descripción y otra de cada aplicación.
3.3 Wix, Webflow y Framer
Wix dice en su centro de ayuda que «Wix sites use Server Side Rendering (SSR)» y avisa: «Most AI crawlers don't run JavaScript». Framer explica que sus páginas «render on the server», y que los avisos de optimización sin resolver al publicar impiden que esa optimización funcione bien: resuélvelos. Webflow publica las páginas que diseñas y genera robots.txt y sitemap automáticamente. El 5 de octubre de 2026 pasamos prueba-html.sh por las portadas de webflow.com, framer.com y wix.com, hechas con su propia herramienta según su etiqueta generator: servían 15.353, 7.862 y 15.214 caracteres de contenido y pasaban el gate A. Es una señal, no una garantía: lo que suele fallar en las tres son los bloques de código personalizado y los widgets de terceros (formularios, reseñas, chats), cuyo texto llega con JavaScript o dentro de un iframe.
3.4 React + Vite: prerender en el build
Una aplicación de React hecha con Vite es una SPA por defecto: su index.html lleva un <div id="root"></div> vacío. Era el caso de esgeo.ai hasta julio de 2026. La solución que usamos es prerenderizar en el build: compilar la aplicación para Node, renderizar cada ruta pública y escribir dist/<ruta>/index.html con el contenido dentro. Simplificado, el package.json queda así:
{
"scripts": {
"build": "vite build && npm run build:ssr && node scripts/prerender.js",
"build:ssr": "vite build --ssr src/entry-server.tsx --outDir dist-ssr"
}
}
El primer paso es el build normal; el segundo compila src/entry-server.tsx para Node, y el tercero escribe el HTML de cada ruta. En esgeo.ai va delante, además, un script que comprueba que la lista de rutas del prerender coincide con la de la aplicación, para no dejarse ninguna. El punto de entrada usa renderToPipeableStream con onAllReady, que la documentación de React recomienda «for crawlers and static generation» porque espera a que todo esté listo (rutas lazy, bloques en Suspense).
// src/entry-server.tsx: renderiza una ruta a HTML en Node, sin navegador
import { Writable } from "node:stream";
import { renderToPipeableStream } from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import { Helmet } from "react-helmet";
import { AppRoutes } from "./App";
export function render(url: string): Promise<{ html: string; head: string }> {
return new Promise((resolve, reject) => {
let html = "";
const sink = new Writable({
write(chunk, _enc, cb) {
html += chunk.toString();
cb();
},
});
sink.on("finish", () => {
const h = Helmet.renderStatic(); // <title>, <meta> y <link> de la ruta
resolve({ html, head: [h.title, h.meta, h.link].join("\n") });
});
const { pipe } = renderToPipeableStream(
<StaticRouter location={url}>
<AppRoutes />
</StaticRouter>,
{
onAllReady() {
pipe(sink); // espera a todo: rutas lazy y Suspense resueltos
},
onShellError: reject,
}
);
});
}
// scripts/prerender.js: escribe dist/<ruta>/index.html con el contenido renderizado
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
const ROUTES = ["/", "/horarios"]; // solo rutas públicas con algo que leer
const dist = path.resolve("dist");
const entry = pathToFileURL(path.resolve("dist-ssr/entry-server.js")).href;
const { render } = await import(entry);
const shell = fs.readFileSync(path.join(dist, "index.html"), "utf-8");
if (!shell.includes('<div id="root"></div>')) {
throw new Error("dist/index.html ya está prerenderizado: ejecuta vite build antes");
}
fs.writeFileSync(path.join(dist, "spa.html"), shell); // shell limpio para rutas SPA
for (const route of ROUTES) {
const { html, head } = await render(route);
const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
if (text.length < 300 || !/<h1[\s>]/.test(html)) {
// el build falla: mejor no publicar que publicar una página vacía
throw new Error(`${route}: render vacío (${text.length} caracteres)`);
}
const page = shell
.replace(/<title>[\s\S]*?<\/title>/, "") // el <title> lo pone la ruta
.replace("</head>", `${head}\n</head>`)
.replace('<div id="root"></div>', `<div id="root">${html}</div>`);
const file = path.join(dist, route, "index.html"); // "/" → dist/index.html
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, page);
console.log(`${route} → ${path.relative(".", file)} · ${text.length} caracteres`);
}
El script comprueba que la plantilla es el shell limpio (si no, copiarías la portada en todas las rutas), guarda ese shell como spa.html para las rutas que siguen siendo SPA (apunta a él el fallback de tu hosting) y hace fallar el build si una ruta sale vacía. Probamos ambos extractos en un proyecto con las mismas versiones que esgeo.ai (React 18.3, React Router 6.27, Vite 5.4): el build escribió el HTML de cada ruta, también el de la ruta lazy, y la página se hidrató sin errores. En el cliente, usa hydrateRoot: según la documentación de React, createRoot «will clear all the existing HTML content» y lo vuelve a crear desde cero.
// src/main.tsx
import { hydrateRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AppRoutes } from "./App";
hydrateRoot(
document.getElementById("root")!,
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
Nuestros tropiezos, por si te pasan: código que usa window o localStorage durante el render (revienta en Node: muévelo a un useEffect) y JSON-LD construido con window.location.href (en el servidor, la URL se saca del router).
3.5 Next.js
Con el App Router, las páginas y los layouts son componentes de servidor por defecto, y según la documentación de Next.js los componentes de cliente ("use client") también se usan para prerenderizar el HTML. Lo comprobamos con Next.js 16.3 en un proyecto nuevo, con un párrafo marcado por cada forma de pintar texto:
| Cómo se pinta el texto | ¿Está en el HTML inicial? |
|---|---|
| Componente de servidor | Sí |
Componente de cliente ("use client") normal | Sí |
Texto que se asigna en un useEffect | No |
Componente cargado con dynamic(..., { ssr: false }) | No |
| Texto que llega como prop, pero solo se pinta tras montar | No, aunque aparece dentro de un script |
Rompen el HTML inicial next/dynamic con { ssr: false } (sirve para «disable prerendering for a Client Component», según la documentación), el contenido calculado en un useEffect y los componentes que devuelven null hasta montarse. El arreglo: pide los datos en el componente de servidor, pásalos como props y quita ssr: false de todo lo que tenga texto.
3.6 Vue y Nuxt
Vue con Vite, sin más, es una SPA: el patrón de 3.4 vale (la guía de SSR de Vite cubre Vue). Nuxt renderiza en servidor por defecto (universal rendering); lo comprobamos con Nuxt 4. Lo rompen ssr: false en nuxt.config (en nuestra prueba, el HTML pasó a llevar <div id="__nuxt"></div> vacío, que HABLA detecta como shell), <ClientOnly> (en el servidor solo sale su #fallback) y el texto asignado en onMounted. Para prerenderizar en el build, Nuxt usa reglas por ruta; en nuestra prueba, esta configuración generó .output/public/index.html con el texto:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
},
})
3.7 Angular
Angular renderiza en el navegador por defecto («This is the default Angular behavior», dice su documentación) y advierte de que eso «may negatively affect search engine optimization (SEO)». En nuestra prueba con Angular 20, el index.html llevaba <app-root></app-root> vacío; HABLA no busca <app-root>, pero, con menos de 300 caracteres, lo marca como shell igualmente. Para añadir renderizado en servidor:
ng add @angular/ssr
En nuestra prueba, ese comando creó src/app/app.routes.server.ts con todas las rutas en modo prerender, y el build generó el HTML con el texto:
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '**',
renderMode: RenderMode.Prerender
}
];
Una trampa: si sirves la aplicación con el servidor de Node que genera Angular, declara tus dominios permitidos (security.allowedHosts en angular.json o la variable NG_ALLOWED_HOSTS). Sin eso, en nuestra prueba con Angular 20 el servidor rechazó la petición por su cabecera Host y cayó a renderizado en cliente: curl recibió <app-root></app-root> vacío. Según la documentación actual, esas peticiones acaban en un error 400. Pasa el guion después de cada despliegue, no solo en local.
3.8 Lo que no debes hacer
No escondas texto pensado solo para las máquinas (oculto con CSS o colocado fuera de la pantalla): si una persona no lo ve, no lo pongas. No sirvas a los bots una página distinta. Y no des por arreglado lo que no has comprobado con el guion.
Hazlo hoy: elige tu vía (CMS, constructor o framework), aplica el arreglo en una página y pásale prueba-html.sh hasta que el gate A pase.
4. robots.txt para bots de IA
robots.txt es un fichero de texto en la raíz de tu dominio que dice a cada rastreador qué puede pedir. Lo define el RFC 9309 (septiembre de 2022), que advierte: «These rules are not a form of access authorization». Lo que no deba ser público, protégelo con contraseña.
4.1 Quién es quién
| Bot (token) | Empresa | Para qué lo usa | ¿Respeta robots.txt? |
|---|---|---|---|
| GPTBot | OpenAI | Entrenamiento | Sí |
| OAI-SearchBot | OpenAI | Búsqueda en ChatGPT | Sí |
| ChatGPT-User | OpenAI | Visita a petición del usuario | «may not apply» |
| ClaudeBot | Anthropic | Entrenamiento | Sí |
| Claude-SearchBot | Anthropic | Búsqueda | Sí |
| Claude-User | Anthropic | Visita a petición del usuario | Sí |
| PerplexityBot | Perplexity | Búsqueda (no entrenamiento) | Sí |
| Perplexity-User | Perplexity | Visita a petición del usuario | «generally ignores» |
| Googlebot | Búsqueda de Google, con AI Overviews y AI Mode | Sí | |
| Google-Extended | Token sin rastreador propio: entrenamiento de Gemini y grounding en Gemini Apps | Sí | |
| Applebot | Apple | Búsqueda en Spotlight, Siri y Safari; sus datos pueden usarse para entrenar | Sí |
| Applebot-Extended | Apple | Token sin rastreador propio: excluirte del entrenamiento | Sí |
| CCBot | Common Crawl | Archivo abierto de la web | Sí |
Matices de las fuentes oficiales: las webs que bloquean OAI-SearchBot «will not be shown in ChatGPT search answers, though can still appear as navigational links», y los cambios en robots.txt pueden tardar hasta unas 24 horas en aplicarse en OpenAI y en Perplexity. Google-Extended «does not impact a site's inclusion in Google Search»: para salir en AI Overviews o AI Mode manda Googlebot, porque la página tiene que estar indexada y poder mostrarse con fragmento. «Applebot-Extended does not crawl webpages», y si tu robots.txt no menciona a Applebot pero sí a Googlebot, Applebot sigue las reglas de Googlebot. El archivo de Common Crawl se usa también para entrenar: en GPT-3, su texto filtrado fue el 60 % de la mezcla de entrenamiento (Brown et al., 2020). HABLA reconoce además Bytespider, de ByteDance, del que no hemos encontrado documentación oficial que citar.
4.2 Decide antes de escribir
- Búsqueda. Si quieres aparecer como fuente en ChatGPT, Claude, Perplexity o Google, permite OAI-SearchBot, Claude-SearchBot, PerplexityBot y Googlebot.
- Visitas de usuario. ¿Quieres que un asistente lea tu página cuando alguien le pega la URL? OpenAI y Perplexity avisan de que estas visitas pueden no respetar robots.txt.
- Entrenamiento. Es una decisión de negocio. Según OpenAI, Google y Apple, bloquear sus bots de entrenamiento no te saca de sus búsquedas.
HABLA no juzga tu decisión: te da 20 puntos de H si tu robots.txt declara, con su propia línea User-agent:, al menos uno de estos diez bots: GPTBot, ClaudeBot, PerplexityBot, OAI-SearchBot, Google-Extended, CCBot, ChatGPT-User, Perplexity-User, Applebot-Extended y Bytespider. Mide que hayas decidido de forma explícita, permitas o bloquees. Una línea comentada no cuenta.
4.3 El error de los grupos
Un robots.txt se organiza en grupos: «One or more user-agent lines that are followed by one or more rules». Según el RFC 9309, cada bot obedece el grupo que coincide con su nombre (sin distinguir mayúsculas) y solo si no encuentra ninguno obedece el grupo * («If no matching group exists, crawlers MUST obey the group with a user-agent line with the "*" value, if present»). Si das a un bot su propio grupo, deja de leer el grupo *, y las prohibiciones para «todos» ya no le aplican. Ejemplo con el error:
User-agent: *
Disallow: /carrito/
Disallow: /mi-cuenta/
User-agent: GPTBot
Allow: /
La intención era «todos fuera del carrito y de la cuenta, y GPTBot, bienvenido». El resultado es que GPTBot también puede entrar en /carrito/ y en /mi-cuenta/, porque solo lee su grupo. Dentro de un grupo gana la coincidencia más específica, la de la ruta más larga («The most specific match is the match that has the most octets»), y si una regla Allow y una Disallow empatan, gana Allow.
4.4 Un robots.txt completo y correcto
Ejemplo para una tienda ficticia que quiere que la citen buscadores y asistentes, que acepta el entrenamiento y que no quiere bots en el carrito ni en el área de cliente:
# Buscadores, búsqueda con IA y visitas que pide un usuario: sí
User-agent: Googlebot
User-agent: Bingbot
User-agent: Applebot
User-agent: OAI-SearchBot
User-agent: ChatGPT-User
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: PerplexityBot
User-agent: Perplexity-User
Disallow: /carrito/
Disallow: /mi-cuenta/
Allow: /
# Entrenamiento de modelos: decisión de negocio (aquí, sí)
User-agent: GPTBot
User-agent: ClaudeBot
User-agent: CCBot
User-agent: Google-Extended
User-agent: Applebot-Extended
Disallow: /carrito/
Disallow: /mi-cuenta/
Allow: /
# Cualquier otro bot
User-agent: *
Disallow: /carrito/
Disallow: /mi-cuenta/
Sitemap: https://www.tudominio.es/sitemap.xml
Varias líneas User-agent: seguidas forman un solo grupo; las rutas privadas se repiten en cada grupo, que es como se evita el error de 4.3. Si no quieres que entrenen con tu contenido, cambia las reglas del segundo grupo por una sola línea Disallow: /. Con este fichero, HABLA encuentra nueve de sus diez bots y suma los 20 puntos.
4.5 Comprueba qué grupo aplica a cada bot
Python 3 trae un analizador de robots.txt con el que puedes probar tu fichero antes de publicarlo. Guarda este programa, crea el robots.txt con el error de 4.3 y ejecútalo:
cat > comprueba-robots.py <<'EOF'
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.parse(open("robots.txt", encoding="utf-8").read().splitlines())
for bot in ["GPTBot", "OAI-SearchBot", "PerplexityBot", "OtroBot"]:
for ruta in ["/", "/carrito/"]:
ok = rp.can_fetch(bot, "https://www.tudominio.es" + ruta)
print(f"{bot:14} {ruta:10} {'permitido' if ok else 'bloqueado'}")
EOF
printf 'User-agent: *\nDisallow: /carrito/\nDisallow: /mi-cuenta/\n\n' > robots.txt
printf 'User-agent: GPTBot\nAllow: /\n' >> robots.txt
python3 comprueba-robots.py
Salida:
GPTBot / permitido
GPTBot /carrito/ permitido
OAI-SearchBot / permitido
OAI-SearchBot /carrito/ bloqueado
PerplexityBot / permitido
PerplexityBot /carrito/ bloqueado
OtroBot / permitido
OtroBot /carrito/ bloqueado
GPTBot entra en el carrito: es el error de los grupos. Con el ejemplo de 4.4 como robots.txt, GPTBot pasa a «bloqueado» en /carrito/, como los demás. Otro analizador, robots-parser para Node.js, que sigue el RFC 9309, dio los mismos resultados. Para probar el tuyo, descárgalo (cambia el dominio) y ajusta los bots y las rutas del programa:
curl -s https://www.esgeo.ai/robots.txt -o robots.txt
4.6 Bloqueos que no están en robots.txt
Tu robots.txt puede permitirlo todo y que los bots no entren, porque algo delante de tu web (el CDN, un cortafuegos, un plugin de seguridad) rechaza ciertos user-agents o direcciones IP. Un caso conocido es Cloudflare: desde el 1 de julio de 2025 pregunta a cada dominio nuevo si quiere permitir los rastreadores de IA, y su documentación indica que desde el 15 de septiembre de 2026 los dominios nuevos bloquean por defecto los bots de entrenamiento y de agentes en las páginas con anuncios (los de búsqueda siguen permitidos). Si usas Cloudflare, revisa la configuración de bots de IA en el apartado de seguridad de su panel.
Para detectarlo, compara la respuesta para un navegador y para el bot:
UA='Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible;'
UA="$UA GPTBot/1.4; +https://openai.com/gptbot"
curl -s -o /dev/null -w '%{http_code}\n' https://www.esgeo.ai/
curl -s -o /dev/null -A "$UA" -w '%{http_code}\n' https://www.esgeo.ai/
Salida:
200
200
Si el primero da 200 y el segundo 403 (o una página de desafío), algo bloquea a ese user-agent. Los bloqueos por IP no se reproducen desde tu ordenador (OpenAI, Perplexity y Common Crawl publican los rangos de IP de sus bots): para eso, revisa los registros del servidor o del CDN. HABLA usa su propio user-agent (HABLA-audit/2.0); si tu web bloquea los que no conoce, te dará un 0 con el aviso «La web bloquea user-agents desconocidos (403)», aunque GPTBot sí entre. En el estudio de octubre de 2026, 29 de las 44 webs que no se pudieron analizar devolvieron un 403 a nuestro auditor (no comprobamos qué hacen con los bots de IA reales).
Por último, el propio robots.txt tiene que responder bien. Si devuelve un error de servidor (5xx), el RFC 9309 dice que el bot «MUST assume complete disallow». Y en una SPA mal configurada, /robots.txt puede devolver tu página HTML en lugar del fichero, y HABLA no lo cuenta:
curl -s -o /dev/null -w '%{http_code} · %{content_type}\n' https://www.esgeo.ai/robots.txt
Salida:
200 · text/plain; charset=utf-8
Hazlo hoy: escribe tu robots.txt con tus tres decisiones, pruébalo con comprueba-robots.py, publícalo y verifica que responde 200 como text/plain. Después compara las respuestas a un navegador y a GPTBot.
5. sitemap.xml y llms.txt
5.1 sitemap.xml con lastmod verdadero
Un sitemap es la lista de URLs que quieres que se rastreen. El protocolo de sitemaps.org exige <urlset>, <url> y <loc>; la etiqueta que más importa aquí es opcional: <lastmod>, la fecha de la última modificación de la página. Es la fecha en que cambió la página, no la del sitemap («the date must be set to the date the linked page was last modified, not when the sitemap is generated»). Google la usa solo si es «consistently and verifiably» exacta y refleja una actualización significativa (cambiar el año del pie no lo es), e ignora <priority> y <changefreq>. Cada sitemap admite hasta 50.000 URLs o 50 MB sin comprimir, y se declara en robots.txt con una línea Sitemap: y la URL absoluta. Ejemplo para un negocio ficticio (.example es un dominio reservado para ejemplos):
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://www.ferreteria.example/</loc>
<lastmod>2026-10-01</lastmod>
</url>
<url>
<loc>https://www.ferreteria.example/catalogo</loc>
<lastmod>2026-09-18</lastmod>
</url>
</urlset>
El error más común es poner la fecha del build en todas las URLs: si todas tus páginas cambiaron hoy, ninguna fecha es creíble. Sácala de donde esté de verdad: la última modificación en tu CMS, el dateModified de los datos estructurados de la página o el último commit que la tocó.
HABLA mira que /sitemap.xml responda 200 y tenga al menos un <loc>: son 15 puntos de H. Lo busca exactamente en /sitemap.xml y sigue las redirecciones, así que, si el tuyo vive en otra ruta, haz que /sitemap.xml redirija a él.
curl -s -o /dev/null -w '%{http_code} · %{content_type}\n' https://www.esgeo.ai/sitemap.xml
curl -s https://www.esgeo.ai/sitemap.xml | grep -c '<loc>'
Salida:
200 · application/xml
43
5.2 llms.txt: qué es y qué no es
llms.txt es una propuesta, no un estándar. La publicó Jeremy Howard el 3 de septiembre de 2024 en llmstxt.org, que la presenta como «A proposal to standardise on using an /llms.txt file». Es un fichero en Markdown, en la raíz del dominio, que resume tu web y enlaza lo importante. Su formato: un H1 con el nombre del sitio (lo único obligatorio); una cita (>) con un resumen corto; opcionalmente, párrafos con más detalle, y secciones con H2 que contienen listas de enlaces (- [nombre](url): nota opcional). Una sección llamada Optional, en inglés por convención, agrupa lo secundario: «links an agent can skip when a shorter context is needed». Ejemplo para el mismo negocio ficticio:
# Ferretería El Ejemplo
> Ferretería El Ejemplo es una tienda de herramientas y material de bricolaje
> pensada para profesionales de la reforma y para particulares. Vende en tienda
> y online, y prepara los pedidos en 24 horas.
Contacto: hola@ferreteria.example. Última actualización: octubre de 2026.
## Páginas principales
- [Catálogo](https://www.ferreteria.example/catalogo): familias y precios con IVA.
- [Envíos y devoluciones](https://www.ferreteria.example/envios): plazos y costes.
- [Horarios](https://www.ferreteria.example/horarios): horarios de la tienda y festivos.
## Optional
- [Aviso legal](https://www.ferreteria.example/aviso-legal)
No hay una lista pública y verificable de qué asistentes leen llms.txt hoy, y Google dice para sus funciones de IA: «You don't need to create new machine readable files, AI text files, or markup to appear in these features». Pero es barato, y HABLA lo puntúa en X: 20 puntos si existe y se sirve como texto, y otros 20 si tiene al menos 200 caracteres y tres enlaces, como el ejemplo.
curl -s -o /dev/null -w '%{http_code} · %{content_type}\n' https://www.esgeo.ai/llms.txt
curl -s https://www.esgeo.ai/llms.txt |
perl -CSD -ne 'chomp; print substr($_, 0, 60), "\n" if /^(# |> |## )/'
Salida:
200 · text/plain; charset=utf-8
# esGEO
> esGEO es la plataforma en español de Generative Engine Opt
## Qué es GEO
## El curso (F1-F5)
## Referencia
## Artículos de fondo
## Herramienta
## Sobre
## Nota sobre este fichero
Si el primer comando te devuelve text/html, tu servidor está sirviendo una página (tu portada o tu error 404) en lugar del fichero, algo típico de las SPA con una reescritura de «todo a index.html». Para HABLA, eso no es un llms.txt.
5.3 Dos etiquetas y un bloque que HABLA también mira
En el <head> suman en H <html lang="es"> (10 puntos) y un canonical absoluto, <link rel="canonical" href="https://..."> (15 puntos), que dice qué URL citar si la página se sirve en varias (con y sin www, con parámetros). HABLA busca rel antes que href, el orden habitual. Con la portada que descargaste en el capítulo 2:
grep -o -E '<html[^>]*lang="[^"]*"' pagina.html
grep -o -E '<link[^>]*rel="canonical"[^>]*>' pagina.html
Salida:
<html lang="es"
<link data-react-helmet="true" rel="canonical" href="https://www.esgeo.ai/"/>
En X, 60 de los 100 puntos no dependen de llms.txt: 30 por un contacto legible (un email en el texto, un enlace mailto: o un ContactPoint en JSON-LD) y 30 por una entidad en JSON-LD (Organization, Person, LocalBusiness, EducationalOrganization o NewsMediaOrganization, con name o url). Lo mínimo, para el negocio ficticio (F4 lo desarrolla):
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Ferretería El Ejemplo",
"url": "https://www.ferreteria.example/",
"email": "hola@ferreteria.example",
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "hola@ferreteria.example",
"availableLanguage": "es"
}
}
En la página va dentro de una etiqueta <script type="application/ld+json">. Antes de publicarlo, comprueba que es JSON válido; si lo guardas como organizacion.json:
node -e 'JSON.parse(require("fs").readFileSync("organizacion.json", "utf8"))' \
&& echo "JSON válido"
Salida:
JSON válido
Con un tipo más específico, como HardwareStore, añade en lista uno de esos cinco para que HABLA vea la entidad: "@type": ["HardwareStore", "LocalBusiness"].
Hazlo hoy: publica sitemap.xml con fechas verdaderas y llms.txt, comprueba con curl que responden 200 y con el tipo correcto, y añade lang, canonical y el bloque Organization a tus páginas.
6. Caso real: esgeo.ai de 35 a 92
Lo que pasó en nuestra propia web, solo con datos que constan en su repositorio y en la web. Al final tienes los comandos para comprobarlo.
6.1 El punto de partida
esgeo.ai se publicó en junio de 2025 (el primer commit del repositorio es del 10 de junio de 2025) como una aplicación de React con Vite: una SPA. Durante todo ese tiempo, el HTML que salía del servidor tenía 237 caracteres de texto. El contenido existía, pero lo pintaba JavaScript, y los rastreadores que no lo ejecutan no lo leían. No lo supimos hasta que construimos HABLA y nos auditamos: 35 sobre 100, MUDA, con el gate A suspendido.
6.2 Cuatro versiones del prerender
El problema no se arregló al primer intento. La historia está en el repositorio, fechada por los commits:
| Fecha | Versión | Qué hacía | Resultado |
|---|---|---|---|
| 20 de marzo de 2026 | v1 | Arrancaba la web en local y guardaba el HTML de cada ruta con una petición normal, sin ejecutar JavaScript | Guardaba el mismo shell vacío en cada ruta |
| 1 de junio de 2026 | v2 | Abría cada ruta con Playwright y capturaba el HTML ya pintado | En local funcionaba; en el build del hosting faltaba una librería del sistema que necesita Chromium |
| 1 de junio de 2026 | v3 | Sin navegador: inyectaba título, metas y JSON-LD con expresiones regulares | El <head>, bien; el <body>, vacío |
| 12 de julio de 2026 | v4 | SSR con renderToPipeableStream en Node | Body completo en las 28 rutas públicas de entonces |
La cabecera del script actual lo resume: la v3 «sólo inyectaba <title>, metas y JSON-LD con regex: el <body> seguía vacío y www.esgeo.ai puntuaba 35/100 (MUDA) en HABLA con el gate A fallido (237 chars de texto)». El commit de la v4, del 12 de julio de 2026, hizo además la higiene de los capítulos 4 y 5: robots.txt con permiso explícito para cada bot de IA, un llms.txt real (el anterior se llamaba llm.txt), canonicals corregidos y un sitemap con fechas. Medido en local, la portada pasó de 237 a unos 8.700 caracteres de texto en el HTML inicial. Ese día, HABLA le dio 92 sobre 100.
6.3 Qué cambió en la nota
Con la rúbrica actual, aquella portada sería un shell: A valdría 15 y el gate cortaría la nota, diera igual lo que hubiera en el <head>. Con más de 3.000 caracteres de contenido, A vale 100, desaparece el tope de 39 y HABLA puede medir también los bloques y el lenguaje.
Una precisión, porque una nota sin rúbrica no es una nota: el 92 del 12 de julio de 2026 se midió con la rúbrica anterior. El 20 de julio de 2026, HABLA pasó a la rúbrica 2.0, que mide hechos en lugar de vocabulario, y la misma web sacó 95, según el registro del proyecto. Al cerrar esta edición, el 5 de octubre de 2026, seguía en 95, con 11.080 caracteres de contenido. Si la auditas hoy, verás la nota de hoy; lo que no debería cambiar es que el gate A pasa.
6.4 Lo que aprendimos (y está en el código)
- Un prerender que no ejecuta tu aplicación no prerenderiza nada. La v1 se llamaba prerender y guardaba el shell. El nombre del script no importa: importa lo que devuelve curl.
- Las metas no son el contenido. La v3 dejó el
<head>perfecto y el<body>vacío. Para un bot, una página con buen título y sin texto sigue siendo una página sin texto. - No dependas de un navegador en el build. La v2 funcionaba en local y no en el entorno de build. El SSR en Node no necesita Chromium.
- Haz que el build falle si una página se vacía. El script actual aborta si una ruta sale con menos de 300 caracteres o sin h1, y exige 3.000 en las rutas clave.
6.5 Compruébalo tú hoy
Con el guion del capítulo 2, compara la portada con /success, que sigue siendo SPA a propósito:
bash prueba-html.sh https://www.esgeo.ai/ | grep -E 'Contenido|Gate'
bash prueba-html.sh https://www.esgeo.ai/success | grep -E 'Contenido|Gate'
Salida (5 de octubre de 2026):
Contenido: 11080 caracteres (guardado en texto.txt)
Gate A de HABLA: pasa
Contenido: 127 caracteres (guardado en texto.txt)
Gate A de HABLA: NO pasa
Después, en https://www.esgeo.ai/geo-score, audita www.esgeo.ai y www.esgeo.ai/success. El 5 de octubre de 2026, la rúbrica daba 95 (BILINGÜE) a la primera y 39 (MUDA) a la segunda. Misma web, mismo robots.txt, mismo llms.txt: la diferencia es si el texto viaja en el HTML.
Este caso no demuestra cuántas veces nos citan las IAs ni cuánto tráfico nos traen: no lo hemos medido aquí y no te daremos cifras que no podemos enseñar. Demuestra que una web puede ser invisible para los rastreadores de IA que no ejecutan JavaScript durante más de un año sin que nadie lo note, que el arreglo se comprueba en un minuto con curl y que la nota de HABLA se mueve en cuanto el texto llega al HTML.
Hazlo hoy: ejecuta los dos comandos de 6.5 contra tu web, cambiando /success por una página de tu sitio que sospeches que llega vacía.
Checklist del módulo
- Has apuntado tu nota de partida en HABLA (nota, fecha y gate A) para tus páginas clave.
- prueba-html.sh dice «pasa» en todas las páginas que quieres que te citen, y cada una tiene un h1 en el HTML inicial.
- No queda ningún contenedor raíz vacío (
root,app,__nexto__nuxt). - El texto importante de pestañas, acordeones y widgets aparece en texto.txt.
- Si prerenderizas, el build falla cuando una ruta pierde su texto o su h1.
- /robots.txt responde 200 como text/plain y refleja tus decisiones sobre búsqueda, visitas de usuario y entrenamiento.
- Ningún bot con grupo propio se salta tus rutas privadas (comprobado con comprueba-robots.py).
- GPTBot recibe el mismo código HTTP que un navegador, y has revisado el ajuste de bots de IA de tu CDN.
- /sitemap.xml responde 200, tiene
<loc>y fechas<lastmod>verdaderas, y está declarado en robots.txt. - /llms.txt responde 200 como texto, con H1, resumen y al menos tres enlaces.
- Cada página lleva
<html lang>y un canonical absoluto. - Hay un email visible o un ContactPoint, y un bloque Organization (o equivalente) en JSON-LD válido.
- Has vuelto a auditar en HABLA y has apuntado la nueva nota con su fecha.
Cómo comprobarlo con HABLA
Abre https://www.esgeo.ai/geo-score, escribe la URL y compara con tu nota de partida. HABLA lee tu HTML sin ejecutar JavaScript, como un rastreador, y te enseña la nota, las cinco dimensiones (H, A, B, L y X), cuántos caracteres de contenido y cuántos h1 encuentra, los cinco hechos de tu primer bloque y los arreglos más urgentes. Esto es lo que mueve F1 en la rúbrica 2.0:
| Lo que has hecho en F1 | Dimensión | Puntos |
|---|---|---|
| Texto en el HTML inicial: 400 caracteres para pasar, 3.000 para el máximo | A (gate) | De 15 a 55, 80 o 100 |
| robots.txt servido como texto, con directivas | H | 20 |
| Directiva para al menos uno de los diez bots de IA | H | 20 |
/sitemap.xml con al menos un <loc> | H | 15 |
<html lang> | H | 10 |
| Canonical absoluto | H | 15 |
| llms.txt servido como texto | X | 20 |
| llms.txt con al menos 200 caracteres y tres enlaces | X | 20 |
| Email, enlace mailto o ContactPoint | X | 30 |
| Organization, Person o LocalBusiness en JSON-LD | X | 30 |
H parte de 20 puntos de base. La nota total pondera H un 20 %, A un 25 %, B un 20 %, L un 25 % y X un 10 %. Si A no pasa o H queda por debajo de 50, la nota no puede pasar de 39. Las franjas son BILINGÜE (80 o más), CONVERSACIONAL (de 60 a 79), BALBUCEA (de 40 a 59) y MUDA (menos de 40).
F1 mueve poco B (un solo h1, encabezados sin saltos, HTML semántico, JSON-LD de contenido, meta description y texto alternativo en las imágenes) y L (la proporción de texto frente al HTML, los cinco hechos del primer bloque y las cifras): eso lo trabajan F2 y F4. Pero hasta que el texto llega al HTML, HABLA no puede medirlos: en /success, B se quedaba en 45 y L en 12.
Un aviso para el primer bloque, que trabajarás en F2: en la rúbrica 2.0, la comprobación de «cifra con unidad» no reconoce los símbolos € ni % (ni con espacio ni sin él), y sí las unidades escritas con letras («47 euros», «10 minutos», «5 módulos»). La de «para quién» busca expresiones concretas, como «para profesionales», «para empresas», «para quien», «si eres» o «pensado para». Escribe así y no perderás puntos por la forma.
Si tu nota no se mueve: con A en 15 o 20, tu texto no llega (repite el capítulo 2 con la URL exacta); si H no sube, revisa con curl robots.txt y sitemap.xml, y si HABLA da un 0 con el aviso de 403, algo delante de tu web rechaza su user-agent (apartado 4.6).
Fuentes
Todas las fuentes se consultaron el 5 de octubre de 2026.
Bots de IA y robots.txt
- OpenAI. «Overview of OpenAI Crawlers». https://developers.openai.com/api/docs/bots
- Anthropic. «Does Anthropic crawl data from the web, and how can site owners block the crawler?» (actualizado el 7 de abril de 2026). https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler
- Perplexity. «Perplexity Crawlers». https://docs.perplexity.ai/guides/bots
- Google. «List of Google's common crawlers». https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers
- Google. «AI features and your website». https://developers.google.com/search/docs/appearance/ai-features
- Apple. «About Applebot». https://support.apple.com/en-us/119829
- Common Crawl. «CCBot» y preguntas frecuentes. https://commoncrawl.org/ccbot · https://commoncrawl.org/faq
- Brown et al. (2020). «Language Models are Few-Shot Learners», tabla 2.2. https://arxiv.org/abs/2005.14165
- RFC 9309, «Robots Exclusion Protocol» (2022). https://www.rfc-editor.org/rfc/rfc9309.html
- Cloudflare. Nota de prensa del 1 de julio de 2025 y documentación «Block AI bots». https://www.cloudflare.com/press/press-releases/2025/cloudflare-just-changed-how-ai-crawlers-scrape-the-internet-at-large/ · https://developers.cloudflare.com/bots/additional-configurations/block-ai-bots/
JavaScript y renderizado
- Vercel (17 de diciembre de 2024). «The rise of the AI crawler». https://vercel.com/blog/the-rise-of-the-ai-crawler
- Google. «Dynamic rendering as a workaround». https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering
- React. «renderToPipeableStream» y «createRoot». https://react.dev/reference/react-dom/server/renderToPipeableStream · https://react.dev/reference/react-dom/client/createRoot
- Vite. «Server-Side Rendering». https://vite.dev/guide/ssr
- Next.js. «Server and Client Components» y «How to lazy load Client Components and libraries». https://nextjs.org/docs/app/getting-started/server-and-client-components · https://nextjs.org/docs/app/guides/lazy-loading
- Nuxt. «Rendering Modes» y «ClientOnly». https://nuxt.com/docs/guide/concepts/rendering · https://nuxt.com/docs/api/components/client-only
- Angular. «Server and hybrid rendering» y «Security». https://angular.dev/guide/ssr · https://angular.dev/best-practices/security
- WordPress. «Template Files». https://developer.wordpress.org/themes/basics/template-files/
- Shopify. «Render essential content in Liquid and HTML, not JavaScript» y «Hydrogen and Oxygen fundamentals». https://shopify.dev/docs/storefronts/themes/best-practices/performance/render-essential-content-server-side · https://shopify.dev/docs/storefronts/headless/hydrogen/fundamentals
- Wix. «Viewing Your Page's Source Code the Way Search Engines Do». https://support.wix.com/en/article/viewing-your-pages-source-code-the-way-search-engines-do
- Framer. «Guide to Framer's hosting infrastructure» y «Address optimization warnings in your project». https://www.framer.com/help/articles/guide-to-framer-hosting-infrastructure/ · https://www.framer.com/help/articles/why-it-s-important-to-fix-optimization-warnings-in-your-project/
- Webflow. «Site-level SEO». https://webflow.com/webflow-way/seo/site-level-seo
sitemap.xml y llms.txt
- sitemaps.org. «Sitemaps XML format». https://www.sitemaps.org/protocol.html
- Google. «Build and submit a sitemap». https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap
- llmstxt.org. «The /llms.txt file» (3 de septiembre de 2024). https://llmstxt.org/
Investigación y datos de esGEO
- Aggarwal et al. (2024). «GEO: Generative Engine Optimization». KDD 2024. https://doi.org/10.1145/3637528.3671900
- esGEO. Estudio de legibilidad para IA de webs españolas (octubre de 2026) y sus datos abiertos. https://www.esgeo.ai/estudio/webs-espanolas-ia-2026 · https://www.esgeo.ai/datos/estudio-webs-espanolas-ia-2026-10.csv · https://www.esgeo.ai/datos/estudio-webs-espanolas-ia-2026-10-errores.csv
- esGEO. HABLA, rúbrica 2.0: https://www.esgeo.ai/habla. Los umbrales y puntos de este módulo salen del código del auditor.
- Repositorio de esgeo.ai: commits del 10 de junio de 2025, del 20 de marzo, del 1 de junio y del 12 de julio de 2026; cabeceras de scripts/prerender.js y src/entry-server.tsx; MASTERPLAN_LOG.md (rúbrica 2.0, 20 de julio de 2026).
Pruebas propias (5 de octubre de 2026)
- Comandos de este módulo contra https://www.esgeo.ai, y rúbrica 2.0 de HABLA sobre su portada y sobre /success.
- prueba-html.sh contra las portadas de webflow.com, framer.com y wix.com.
- Proyectos creados desde cero: React 18.3 + Vite 5.4 con el prerender de 3.4, Next.js 16.3, Nuxt 4 y Angular 20 con @angular/ssr.
- Analizadores de robots.txt: urllib.robotparser (Python 3.13) y robots-parser (Node.js).
Texto bajo licencia CC BY-NC-SA 4.0: puedes copiarlo, adaptarlo y compartirlo citando a esGEO, sin uso comercial y con la misma licencia. Volver al índice del curso.