Devlog · Fase 0 · 7/7
SEO multilingüe y GEO: 6 locales, SSR y extracción por IA
Seis URL indexables por idioma, un hreflang BCP47 bidireccional, un grafo JSON-LD enlazado por @id y un llms.txt: cómo aubia.dev se da a leer a los motores como a las IA.
- seo
- i18n
- geo
- inertia

Un sitio web previo al lanzamiento tiene un único cometido en el frente de la visibilidad: ser localizable, en varios idiomas, por los motores clásicos y por los motores generativos que ahora responden en lugar de los diez enlaces azules. aubia.dev se sirve en seis idiomas, con renderizado en servidor, con un marcado pensado para la extracción. Este último artículo de la serie desmonta esa maquinaria. Y el blog que usted lee gira sobre las mismas reglas.
Seis locales, una URL por idioma
El sitio se enruta bajo un prefijo de locale: /fr, /en, /es, /de, /it, /pt. Cada idioma tiene por tanto su propia URL indexable, nunca un parámetro o una cookie que ocultase el contenido a los robots. Un middleware resuelve el locale antes del enrutamiento, a partir del segmento de la URL, la cookie, y luego la cabecera Accept-Language. La raíz / redirige al mejor idioma detectado.
Para la audiencia internacional fuera de las regiones cubiertas, x-default apunta al inglés. El renderizado se hace en SSR mediante Inertia: el HTML completo sale en el primer byte, lo cual es un invariante innegociable para el SEO. Un motor que visita una página recibe su contenido renderizado, no una cáscara vacía a la espera de la hidratación de JavaScript.
Estos seis idiomas no tienen todos el mismo estatus, y asumo la jerarquía. El francés es mi fuente editorial, escrita a mano. El inglés es un pivote traducido manualmente, porque es el idioma de mi audiencia internacional y no tolera la aproximación. El español, el italiano, el alemán y el portugués se traducen luego automáticamente a partir de ese pivote inglés. Y el texto más crítico, el título del hero, el claim, los argumentos clave, queda excluido de la traducción automática y siempre pasa por mi mano.
Detrás de estos seis idiomas, no hay ninguna librería i18n de terceros. La solución npm dominante resultó incompatible con React 19 y arriesgaba filtrar estado entre peticiones bajo el servidor de larga duración de Octane. Por eso el motor i18n cabe en un hook casero de una cincuentena de líneas, conectado al reparto nativo de props de Inertia, sin una sola dependencia añadida.
El hreflang, en ambos sentidos
El hreflang le dice a un motor: esta página existe también en estos otros idiomas, en estas URL precisas. La regla que hace fracasar a la mayoría de las implementaciones es la bidireccionalidad: cada versión debe referenciarse a sí misma y referenciar a todas las demás. Uso códigos BCP47 completos, más precisos que un código corto, para levantar la ambigüedad de país entre pt-PT y pt-BR.
El sitemap dinámico produce esta malla para cada página publicada, con un enlace alternate por locale:
foreach (self::LOCALE_TO_BCP47 as $altLocale => $bcp47) {
$altLoc = $baseUrl . '/' . $altLocale . $path;
$xml .= '<xhtml:link rel="alternate" hreflang="' . $bcp47 . '" href="' . $altLoc . '"/>';
}
Del lado de la página, el componente SeoHead coloca las mismas señales en el <head>, pero solo en las páginas indexables. Las páginas efímeras, como la confirmación de la lista de espera, son noindex: añadir ahí un canonical o un hreflang enviaría una señal contradictoria.
Un grafo JSON-LD enlazado por @id
Los datos estructurados de Aubia forman un solo @graph, donde cada entidad se referencia por un @id estable: la Organization, el WebSite, la SoftwareApplication y la Person fundadora se apuntan mutuamente. Este cableado ayuda a los motores y a las IA de búsqueda a entender que estos objetos describen la misma entidad, y no cuatro cosas sin relación.
const jsonLd = JSON.stringify({
'@context': 'https://schema.org',
'@graph': [
{ '@type': 'Organization', '@id': `${SITE_URL}/#organization`, /* ... */ },
{ '@type': 'Person', '@id': `${SITE_URL}/#founder`, /* ... */ },
{ '@type': 'WebSite', '@id': `${SITE_URL}/#website`, /* ... */ },
...additionalJsonLd,
],
});
El punto de extensión es additionalJsonLd: cada página inyecta sus propios nodos en el grafo compartido. La landing añade su bloque FAQPage. Un artículo de blog añade su BlogPosting y su breadcrumb, sin tocar la base Organization/WebSite.
GEO: escribir para los motores que leen
Escribí estas páginas para ser citadas, no solo posicionadas: cuando una IA responde en lugar de los diez enlaces azules, lo que cuenta es que pueda extraer un pasaje entero sin deformarlo. Dos palancas concretas en aubia.dev.
La primera es un archivo public/llms.txt en la raíz: un resumen estructurado del producto, de su posicionamiento y de su estado, escrito para ser leído por una máquina. Declara qué es Aubia, su diferencia de cross-review, su público y su hoja de ruta, en texto plano sin ruido de interfaz.
La segunda es una regla de escritura, aplicada a este mismo artículo: secciones autosuficientes. Cada título H2 abre con una definición, luego se desarrolla en un bloque de unas ciento cincuenta palabras que se sostiene solo, fuera del contexto de la página. Un motor que trocea la página en extractos debe poder citar cualquier bloque sin que el sentido se derrumbe. Listas cortas y títulos explícitos rematan el trabajo.
El bucle meta: este blog se aplica sus propias reglas
Este blog es en sí mismo la demostración de todo lo anterior. Es flat-file: cada artículo es un archivo Markdown en el repositorio, convertido a HTML en el parseo, sin base de datos, fiel a la postura de la Fase 0. Publicar un artículo es fusionar una rama, nada más.
La publicación se programa por la fecha del front matter: el módulo conserva todos los artículos en caché, pero filtra en la petición aquellos cuya fecha ha pasado. Un artículo fechado un lunes aparece ese mismo lunes, sin redespliegue, y permanece invisible antes: ausente del índice, del feed RSS, del sitemap, y un 404 en su URL.
Cada artículo publicado se gana su entrada de sitemap con su fecha real de última modificación, sus alternativas de idioma, su marcado BlogPosting y su breadcrumb. La misma maquinaria de seis locales, de hreflang bidireccional y de grafo JSON-LD que describí para la landing se aplica, artículo por artículo, a estas páginas.
Esta fue la última entrega de la serie sobre la construcción de la Fase 0. Construí toda la Fase 0 a este nivel de detalle, y sus comentarios decidirán lo que sigue. La lista de espera es el lugar para seguirla de cerca.
Esta obra se cuenta aquí, artículo a artículo. Súbase a bordo: sus comentarios dibujarán lo que viene.
Únase a la lista de espera