Ir al contenido principal

Devlog · Fase 0 · 7/7

El SEO multilingüe de aubia.dev: URL, hreflang y JSON-LD

Los seis idiomas de aubia.dev tienen URL y enlaces hreflang propios. El HTML servido vincula los artículos con su autor, sin garantizar su indexación ni que una IA los cite.

Publicado el 24 de agosto de 2026Actualizado el 15 de septiembre de 202611 min de lectura
  • seo
  • i18n
  • geo
  • inertia

Una visita a /fr/blog/multilingual-seo-geo debe recibir este artículo en francés, aunque el navegador prefiera el inglés. La cookie de idioma tampoco debe cambiar el contenido de esa URL. Para compartir una traducción o conseguir que se indexe, necesito que su dirección identifique siempre el mismo idioma.

Las páginas públicas de aubia.dev utilizan seis prefijos: /fr, /en, /es, /de, /it y /pt. Los slugs se mantienen en inglés en todas las versiones. El prefijo basta para distinguirlas.

El renderizado en el servidor de Inertia proporciona el contenido y los metadatos en la respuesta HTML inicial. Un rastreador puede leerlos sin ejecutar el JavaScript de la página. Esto depende de que el SSR funcione: si falla, la configuración del sitio permite recurrir al renderizado en el cliente. El contenido depende entonces de JavaScript, como explica en detalle el artículo dedicado a esta stack.

El idioma de la URL y el del navegador

El middleware SetLocale selecciona el idioma antes del renderizado. Consulta el segmento de la URL, después la cookie de preferencia, la cabecera Accept-Language del navegador y, por último, la configuración de la aplicación. Utiliza el primer valor reconocido.

En una página bajo /fr, el segmento de la URL tiene siempre prioridad. En / no hay segmento de idioma: la cookie, las preferencias del navegador y el idioma por defecto, el inglés, determinan el destino de la redirección.

Para leer Accept-Language, el servidor recorre los idiomas por orden de preferencia y compara sus dos primeras letras con los seis idiomas admitidos. Si no hay una cookie que tenga prioridad, un navegador que solicite pt-BR puede redirigirse a /pt.

La raíz responde con una redirección 302. Una 301 indicaría un traslado permanente, aunque el destino depende del visitante y de su preferencia en ese momento.

Una respuesta que varía según la petición

Una caché compartida no debe reutilizar una redirección al francés para un visitante que solicita italiano. La cabecera Vary indica qué cabeceras de la petición distinguen las respuestas.

Inertia establece Vary: X-Inertia para separar sus respuestas HTML y JSON. Su middleware sustituye esta cabecera, por lo que SetLocale añade los valores relacionados con el idioma después de que ese middleware devuelva la respuesta, solo en la ruta de redirección:

if ($request->routeIs('home.redirect')) {
    $response->headers->set('Vary', ['Cookie', 'Accept-Language'], replace: false);
}

La respuesta incluye entonces X-Inertia, Cookie y Accept-Language. Las páginas localizadas conservan el Vary de Inertia, sin añadir valores para la negociación del idioma: su URL ya lo determina.

Este mecanismo depende de la caché que recibe la respuesta. Cloudflare no tiene en cuenta todos los valores de Vary por defecto. Por tanto, enviar la cabecera no demuestra por sí solo que el CDN distinga estas variantes; sus reglas de caché deben ser compatibles con las respuestas servidas.

Hreflang vincula las traducciones

Una URL distinta por idioma permite acceder a cada versión de forma independiente. La anotación hreflang indica a los buscadores qué páginas son traducciones entre sí.

Cada versión enumera su propia URL y las de los demás idiomas disponibles. Google exige estos enlaces recíprocos: si dos páginas no se referencian mutuamente, las anotaciones de ese par pueden ignorarse. Los pares que sí se enlazan entre sí pueden seguir procesándose.

Utilizo solo códigos de idioma: fr, en, es, de, it y pt. El sitio ofrece una versión en portugués destinada a los hablantes de ese idioma, no contenidos separados para Portugal y Brasil. pt describe esa elección; pt-PT indicaría contenido en portugués destinado a Portugal.

La anotación informa al buscador del público al que se dirige el contenido. La redirección HTTP de la raíz elige el destino de una visita según la petición recibida.

Una versión alternativa por defecto

La anotación x-default identifica una versión alternativa para los idiomas o regiones no cubiertos. En aubia.dev apunta al inglés. Si la traducción inglesa de un artículo no está publicada, se omite: una URL que devuelve 404 no sería un destino alternativo útil.

Las anotaciones del blog se limitan a las traducciones publicadas del mismo slug. Un artículo disponible en tres idiomas enumera esas tres versiones, aunque el resto del sitio admita seis.

El componente SeoHead escribe estos enlaces en el <head>. El sitemap también los genera a partir de los idiomas publicados. Los dos métodos son equivalentes para Google; utilizar ambos no aporta ninguna ventaja de posicionamiento. Mantener los dos exige, sobre todo, que sus destinos coincidan.

Una URL canónica por idioma

El enlace canonical responde a otra pregunta: ¿qué URL debe preferirse entre páginas idénticas o muy similares? Una traducción completa no es un duplicado del original solo por tratar el mismo tema en otro idioma.

Por tanto, cada versión de un artículo declara su propia URL canónica. La página francesa no designa la inglesa como canónica. Google recomienda un destino en el mismo idioma cuando existe y se reserva la decisión final sobre qué URL seleccionar.

Las páginas de confirmación y de baja utilizan noindex,nofollow. El sitio no les añade enlaces canonical ni hreflang y las excluye del sitemap. Así, las anotaciones de versiones por idioma se reservan a las páginas destinadas a la indexación.

Open Graph utiliza otro formato: og:locale vale, por ejemplo, fr_FR. La especificación Open Graph define language_TERRITORY para esta propiedad opcional. Ese formato con territorio no se traslada a la segmentación de hreflang.

Enlaces de idioma disponibles antes de JavaScript

El selector de idioma contiene enlaces reales a las otras versiones de la página. Google recomienda estos enlaces junto con las anotaciones hreflang para que los visitantes puedan elegir por sí mismos.

El componente utiliza los elementos HTML nativos details y summary. Las anclas se renderizan incluso cuando el panel está cerrado. Con SSR, por tanto, están presentes en el HTML recibido, sin esperar a que un clic las añada al documento.

El navegador gestiona la apertura con el ratón o el teclado. Un efecto React añade el cierre al pulsar Escape y al hacer clic fuera. Un menú que solo monta su contenido al abrirse no proporcionaría estos enlaces en el renderizado inicial; esa limitación depende del comportamiento del componente, no de la mera presencia de un portal React.

Los destinos proceden del servidor. En un artículo se restringen a las traducciones publicadas, igual que los hreflang. El selector no ofrece un idioma en el que falte el artículo.

Un autor identificado en el grafo JSON-LD

El componente SeoHead describe tres entidades comunes en JSON-LD: la organización Aubia, yo como persona y el sitio web. JSON-LD utiliza aquí el vocabulario de schema.org para nombrar tipos y relaciones.

Cada entidad tiene un @id estable. Las referencias a estos identificadores declaran una relación sin repetir el objeto entero. El grafo incluye los siguientes vínculos, mostrados en un fragmento limitado a tipos, identificadores y relaciones:

{
    "@context": "https://schema.org",
    "@graph": [
        {
            "@type": "Organization",
            "@id": "https://aubia.dev/#organization",
            "founder": { "@id": "https://aubia.dev/#founder" }
        },
        {
            "@type": "Person",
            "@id": "https://aubia.dev/#founder",
            "worksFor": { "@id": "https://aubia.dev/#organization" }
        },
        {
            "@type": "WebSite",
            "@id": "https://aubia.dev/#website",
            "publisher": { "@id": "https://aubia.dev/#organization" }
        }
    ]
}

Las páginas añaden sus datos mediante additionalJsonLd. La página de inicio proporciona un FAQPage con las preguntas y respuestas visibles. Un artículo añade un BlogPosting y un BreadcrumbList, su hilo de Ariadna.

En el BlogPosting, author referencia #founder y publisher referencia #organization. El autor del artículo queda así declarado como la misma persona que el fundador del sitio. Añadir un objeto a @graph no crea, por sí solo, una relación con todos los demás objetos: las propiedades y sus referencias describen esas relaciones.

La firma visible de cada artículo enlaza al mismo perfil público que el nodo Person. Los metadatos Open Graph declaran el tipo article y sus fechas de publicación y modificación.

El grafo no declara SoftwareApplication ni una oferta de preventa: el sitio ofrece una lista de espera, no una descarga de software. No se inventan reseñas ni valoraciones para obtener un resultado enriquecido. Las preguntas frecuentes visibles conservan su marcado, identificado por la URL de la página localizada, pero Google no muestra resultados enriquecidos de FAQ desde el 7 de mayo de 2026. Estas declaraciones no garantizan su uso por un buscador ni la indexación.

GEO, sin garantía de cita

El término GEO, de Generative Engine Optimization, designa las prácticas destinadas a mejorar la visibilidad de un contenido en las respuestas generadas por IA. No es un protocolo común que garantice que una página se lea, se seleccione o se cite.

Para aparecer como enlace de referencia en AI Overviews o AI Mode de Google Search, una página debe estar indexada por Google y ser apta para mostrarse con un fragmento. No se requiere ningún marcado especial de schema.org. Incluso una página que cumple estas condiciones no tiene garantizado aparecer como fuente.

Intento escribir secciones que se entiendan sin tener que releer todo el artículo. Eso ayuda a seguir una explicación y a citar un pasaje, sin garantizar que una IA lo reproduzca fielmente.

Un archivo llms.txt que mantener

El sitio también sirve un archivo llms.txt. Contiene una presentación del producto en Markdown y enlaces a páginas públicas, sin una interfaz por la que navegar. Se basa en la propuesta llms.txt, distinta de las reglas de rastreo de robots.txt.

Este archivo se escribe por separado del sitio y de sus artículos. Por eso hay que comprobar sus afirmaciones y enlaces cuando cambian en otros lugares. Su presencia no demuestra que un buscador lo lea ni que mejore el posicionamiento del sitio o sus citas.

La publicación de las traducciones

Cada artículo del blog tiene un archivo Markdown por idioma. Su fecha de publicación se lee del front matter, en UTC. El build compila el Markdown y el resaltado Phiki en un catálogo local obligatorio en producción, incluidos los artículos futuros. Las fechas se comprueban en cada lectura: antes de la fecha, la URL devuelve 404; una vez alcanzada, el artículo es accesible sin otro despliegue. Un catálogo ausente, inválido o desactualizado produce una respuesta 503 en las rutas que dependen de él, sin ejecutar Phiki durante la petición.

El índice lo incluye y el feed RSS publica su entradilla con la fecha de publicación. El sitemap utiliza la fecha de actualización, o la de publicación si no se ha indicado ninguna, para lastmod. Añade las anotaciones hreflang de las traducciones disponibles. La propia página proporciona el BlogPosting y el hilo de Ariadna.

El feed RSS y el sitemap se sirven sin sesiones ni cookies y especifican una duración de caché HTTP de una hora. Su actualización en una caché intermedia o en un lector de feeds puede retrasarse respecto a la disponibilidad del artículo en la aplicación. El lastmod de cada índice del blog corresponde a la fecha de modificación o publicación más reciente de sus artículos publicados. Las páginas estáticas no declaran una fecha desconocida. El <head> de las páginas del blog incluye también un enlace de descubrimiento al feed RSS localizado.

Los textos de la interfaz siguen otro proceso

Los textos de la interfaz se almacenan en archivos lang/<locale>.json. El francés es la fuente y el inglés, el pivote para el español, el alemán, el italiano y el portugués.

Al hacer commit de un cambio en el archivo francés, el hook envía al agente de traducción las claves añadidas o modificadas que no están excluidas. Combina los valores recibidos con los del archivo inglés y conserva los demás. Las eliminaciones de claves se aplican por separado. La propagación del inglés a los otros cuatro idiomas se inicia con un comando manual.

Tres prefijos están excluidos de las adiciones y modificaciones automáticas: usp.*, features.tagline.* y letter.body.*. Los dos primeros no tienen claves en los diccionarios actuales. El tercero protege el cuerpo de la Carta del fundador. El claim y el título del banner de inicio no figuran entre estas exclusiones, por lo que pueden traducirse automáticamente.

El servidor carga las traducciones del idioma solicitado y completa las claves que faltan con el inglés. El hook React useT() lee después los valores compartidos en las props de Inertia. Los artículos no pasan por este script: sus archivos Markdown se traducen por separado.

El cuerpo de la Carta solo existe en los diccionarios francés e inglés. En español, alemán, italiano y portugués, sus párrafos aparecen por tanto en inglés, aunque los títulos y las etiquetas que los rodean estén traducidos. Su contenedor declara lang="en": esta anotación identifica su idioma sin traducirlos.

Esta obra se cuenta aquí, artículo a artículo. Lo que viene después depende de lo que usted diga de ella.

Únase a la lista de espera