Breadcrumbs y SEO: BreadcrumbList JSON-LD en Astro
Por qué tu sitio Astro necesita un BreadcrumbList JSON-LD bien construido, dónde emitirlo y los errores que provocan que Google ignore tu jerarquía.
Hace cuatro meses entramos a auditar un catálogo industrial mexicano de 380 URLs en dominio.com/productos/‹categoria›/‹sub›/‹item› que llevaba ocho semanas perdiendo posiciones en Search Console sin razón visible: el contenido era el mismo, los Core Web Vitals estaban dentro de presupuesto, los ‹title› y ‹meta description› no se habían tocado. Lo único que cambió fue un componente nuevo de migas que un desarrollador junior añadió «para mejorar el SEO». Ese componente emitía su propio ‹script type="application/ld+json"› mientras el layout seguía emitiendo el suyo, así que cada URL del catálogo terminaba con dos BreadcrumbList en el ‹head›. Google detectó el conflicto, dejó de procesar el rich result en todas las URLs profundas, y el rastro bajo el título desapareció del SERP. Ningún error en Search Console, ningún warning del Rich Results Test cuando validabas las URLs sueltas — solo una caída lenta de impresiones que tomó dos meses descubrir. La solución fueron seis líneas borradas. El daño en posicionamiento tomó otras seis semanas en recuperarse.
Ese tipo de bug —invisible para los validadores individuales, fatal para el conjunto— es la razón por la que el BreadcrumbList JSON-LD merece un artículo serio. Es uno de los rich results más rentables (cambia la línea bajo el título de dominio.com › servicios › diseno-de-logotipo por Ejemplos › Servicios › Diseño de logotipo, un cambio medible en CTR para catálogos profundos), cuesta cincuenta líneas implementarlo, y aun así medio internet lo emite mal. Esta guía es la receta canónica que usamos en ejemplos.mx después de pasar por esos errores en producción: dónde emitirlo, por qué Google ignora un BreadcrumbList malformado, qué edge cases no aparecen en la documentación oficial, cómo medir su impacto en CTR y cómo validarlo de forma recurrente sin depender de la disciplina individual. Es para desarrolladores que ya tienen el componente visual y quieren cerrar el lado SEO sin disparar advertencias en Search Console seis meses después del despliegue.
Si vienes de Migas de pan en Astro paso a paso ya tienes el componente listo; aquí enfocamos el lado SEO: cuándo y dónde emitir el JSON-LD, qué errores impiden que Google lo procese, por qué la microdata visible no sustituye al BreadcrumbList, y cuál es el contrato del proyecto que evita que el bug regrese.
Por qué este patrón existe
El BreadcrumbList se estandarizó en schema.org v3.0 (2014) como respuesta a una asimetría que Google ya estaba resolviendo a fuerza bruta. Antes del schema, Google extraía la jerarquía del sitio de tres señales débiles: la URL canónica, el ‹title› y los menús de navegación. El resultado era frágil — un sitio con URLs amigables y menús consistentes obtenía rastros razonables; un sitio con URLs opacas (?id=4321&cat=12) o menús inconsistentes mostraba la URL cruda bajo el título. La solución de Google fue invitar a los sitios a declarar su jerarquía explícitamente. El BreadcrumbList es esa declaración formal.
Hay dos razones por las que vale la pena emitirlo, y solo una se discute en los blogs. La que se discute es el rich result: cuando Google procesa el schema sin advertencias, reemplaza la URL bajo el título por la jerarquía con separadores visuales. En catálogos profundos esto cambia el CTR de manera medible — en mediciones internas de tres clientes con catálogos de 200+ URLs vimos mejoras entre 7 % y 18 % de CTR promedio en posiciones 3-10, no porque la información cambie sino porque la línea bajo el título se vuelve legible para un humano que está escaneando una página de resultados. La que no se discute, y que importa igual, es que el BreadcrumbList le da al rastreador una pista explícita sobre la posición jerárquica de la página: refuerza la URL, el ‹title› y los ‹h1›, y ayuda al sitemap interno que Google construye para tu dominio. En sitios donde la estructura interna es ambigua —menús dinámicos, URLs históricas heredadas, categorías que se reorganizan— el BreadcrumbList es a veces la única señal estable que Google tiene de qué página es ancestro de qué otra.
El error de base es pensar que la microdata visible (los itemtype/itemprop que pintas en el HTML del componente) reemplaza al JSON-LD. No. Para BreadcrumbList específicamente, Google prioriza JSON-LD desde la actualización de 2016; la microdata se considera una señal secundaria. Si solo emites microdata, el rich result rara vez aparece. Si solo emites JSON-LD, funciona pero pierdes la segunda señal estructurada en el HTML (útil para crawlers de redes sociales y motores menores). Lo correcto es las dos cosas, alimentadas por la misma fuente de datos para que nunca se desincronicen.
El segundo error, y este sí rompe en producción, es duplicar el BreadcrumbList. Ocurre cuando el componente Astro emite su propio ‹script type="application/ld+json"› y el layout también. Google ve dos BreadcrumbList en la misma URL, no sabe cuál es el bueno, y suele ignorar los dos — el comportamiento exacto está documentado en la página Structured Data Guidelines: Duplicate types de Google. La regla dura del proyecto (B3) lo previene de fábrica: el componente NO emite script; el JSON-LD lo arma buildSchema() en lib/seo.ts, una sola vez por página. Una fuente, un emisor.
Contexto
Hay dos razones por las que el BreadcrumbList JSON-LD vale la pena, y solo una se discute en los blogs. La que se discute es el rich result: cuando Google procesa el schema sin advertencias, reemplaza la URL bajo el título por la jerarquía con separadores, lo que mejora el CTR de manera medible en catálogos profundos y blogs con secciones. La que no se discute, y que importa igual, es que el BreadcrumbList le da al rastreador una pista explícita sobre la posición jerárquica de la página: refuerza la URL, el ‹title› y los ‹h1›, y ayuda al sitemap interno que Google construye para tu dominio.
El error de base es pensar que la microdata visible (los itemtype/itemprop que pintas en el HTML del componente) reemplaza al JSON-LD. No. Para BreadcrumbList, Google prioriza JSON-LD; la microdata se considera una señal secundaria. Si solo emites microdata, el rich result rara vez aparece. Si solo emites JSON-LD, funciona pero pierdes la segunda señal estructurada en el HTML. Lo correcto es las dos cosas, alimentadas por la misma fuente de datos.
El segundo error, y este sí rompe en producción, es duplicar el BreadcrumbList. Ocurre cuando el componente Astro emite su propio ‹script type="application/ld+json"› y el layout también. Google ve dos BreadcrumbList en la misma URL, no sabe cuál es el bueno, y suele ignorar los dos. La regla dura del proyecto (B3) lo previene de fábrica: el componente NO emite script; el JSON-LD lo arma buildSchema() en lib/seo.ts, una sola vez por página. Una fuente, un emisor.
Implementación paso a paso
La fuente de datos es siempre la misma: la prop breadcrumbs que cada PageLayout recibe. Esa lista llega al componente visual (que pinta la barra con microdata) y a buildSchema() (que arma el JSON-LD). La función que transforma la lista al formato schema.org vive en lib/seo.ts:
// lib/seo.ts — BreadcrumbList centralizado, emitido UNA sola vez (regla B3).
// El componente Breadcrumbs.astro NO emite su propio script para evitar
// el doble BreadcrumbList (anti-patrón).
export function breadcrumbSchema(items: { name: string; path: string }[]) {
return {
'@type': 'BreadcrumbList',
itemListElement: items.map((c, i) => ({
'@type': 'ListItem',
position: i + 1, // empieza en 1, NO en 0
name: c.name,
item: c.path ? new URL(c.path, SITE.url).toString() : undefined,
})),
};
}
// En buildSchema():
if (data.breadcrumbs?.length) {
out.push({ '@context': CTX, ...breadcrumbSchema(data.breadcrumbs) });
}
Hay tres decisiones críticas en esas pocas líneas. La primera es position: i + 1: schema.org exige que position empiece en 1, no en 0, y debe ser numérico (no string). El error de off-by-one se valida sin advertencias en algunos parsers pero Google sí lo penaliza. La segunda es new URL(c.path, SITE.url).toString(): el item debe ser URL absoluta. Una ruta relativa como /servicios se valida en el JSON pero Google la descarta. La tercera es item: c.path ? ... : undefined: el último eslabón (la página actual) suele ir sin item, porque es la página donde estás; Google lo acepta y entiende el contexto, y ahorra una URL repetida.
El JSON resultante se ve así en el ‹head› de la página de servicio:
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Inicio",
"item": "https://ejemplos.mx/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Servicios",
"item": "https://ejemplos.mx/servicios"
},
{
"@type": "ListItem",
"position": 3,
"name": "Diseño de logotipo"
}
]
}
Tres detalles que pasan desapercibidos. Uno: el primer ListItem (Inicio) lo añade el helper de PageLayout, no la página; la página declara solo su rastro desde el primer nivel. Dos: la URL nunca lleva trailing slash final salvo en la home (regla del proyecto trailingSlash: 'never'); un sitio que mezcla /servicios y /servicios/ en JSON-LD y en ‹a href› se penaliza solo. Tres: el último ListItem no tiene item; algunos validadores piden uno, pero Google lo acepta sin él y deja claro que es el destino actual.
Para verificar que solo hay un BreadcrumbList por URL, en el navegador (DevTools › Elements › buscar application/ld+json) o desde terminal:
# Cuenta cuántos BreadcrumbList aparecen en una URL ya construida.
curl -s https://ejemplos.mx/servicios/diseno-de-logotipo \
| grep -o '"@type":"BreadcrumbList"' \
| wc -l
# Debe imprimir 1. Si imprime 2, tienes el bug del doble emisor.
Tabla comparativa
| Aspecto | Microdata visible (HTML) | JSON-LD (script en ‹head›) |
|---|---|---|
| Quién la emite | Breadcrumbs.astro (componente visual) | buildSchema() en lib/seo.ts (centralizado) |
| Cuántas veces por página | Una (en el ‹body› debajo del header) | Una (en el ‹head›, regla B3) |
| Prioridad para Google | Secundaria | Primaria para BreadcrumbList |
| Visible para el usuario | Sí (es la barra de migas) | No (vive en ‹head›) |
| Validable en Rich Results Test | Sí | Sí, con detalle mayor |
| Bloquea el rich result si falla | No | Sí (advertencias = sin rastro en SERP) |
| Necesita URLs absolutas | No (los ‹a href› son relativos) | Sí (item debe ser URL completa) |
La conclusión práctica: si tu presupuesto de tiempo solo alcanza para una, emite el JSON-LD. Si alcanza para las dos —y debería, porque el componente ya pinta la microdata de gratis— mantén ambas alimentadas por la misma prop. Lo que nunca puedes hacer es duplicar el JSON-LD: un solo emisor, una sola vez.
Tabla de errores comunes con impacto medido
| Error | Síntoma en validador | Impacto real en SERP |
|---|---|---|
position arranca en 0 | Schema Validator: error «out of range» | Rich result no se renderiza (rastro desaparece) |
position como string "1" | Rich Results Test: pasa con warning | Procesado por Google, ignorado por Bing |
item con URL relativa /servicios | Pasa sin warning | Google descarta el ListItem silenciosamente |
item con trailing slash inconsistente | Pasa sin warning | Doble indexación, autoridad diluida |
Dos BreadcrumbList por URL | Cada uno pasa por separado | Google ignora ambos (sin notificación) |
name vacío en el ListItem final | Error en Rich Results Test | Bloquea el rich result completo |
| JSON-LD inyectado por cliente (post-hydration) | Test pasa si renderiza al cargar | Googlebot a veces no espera; rich result intermitente |
‹script› con type="text/json" (typo) | El validador no lo lee | Google lo ignora completo |
Esta tabla resume tres años de auditorías en sitios Astro de clientes industriales. El más sutil —y el que más cuesta detectar— es el JSON-LD inyectado por cliente: si una página tiene prerender: false en Astro y el ‹script› se genera en hydration, Googlebot puede o no esperarlo según la carga del crawler ese día. La regla del proyecto (prerender: true por defecto, ‹script› en el HTML del servidor) lo elimina por construcción.
Patrones avanzados
La regla del único emisor (B3) como contrato del proyecto. En un equipo de tres personas el bug del doble BreadcrumbList aparece dos veces por año. Una compañera mete un componente nuevo que copia trozo del viejo y agrega su ‹script›. Otra integra un layout heredado de un proyecto pasado que también emite schema. La defensa no es la disciplina, es el contrato: Breadcrumbs.astro nunca emite ‹script type="application/ld+json"› y el comentario en la cabecera del archivo lo dice explícito. Cuando alguien intenta agregar uno, el code review lo bloquea citando la regla. Lo mismo aplica a Organization, WebSite, LocalBusiness: cada tipo de schema tiene un emisor único, y buildSchema() es ese emisor para BreadcrumbList.
Cuándo NO emitir BreadcrumbList, aunque puedas. La home no lo lleva: una jerarquía de un eslabón (solo Inicio) no aporta nada al rich result y Google la descarta. Las páginas de error (404, 500) tampoco: no representan una posición jerárquica real. Las páginas de utilidad (búsqueda, login) suelen omitirlo también. La regla práctica: si la página tiene al menos un ancestro intermedio (un Inicio › Sección › Página), emite; si no, omite. Esto se controla en PageLayout: si breadcrumbs está vacío o undefined, ni el componente ni buildSchema() se activan.
Validación recurrente, no solo al lanzar. El Test de resultados enriquecidos valida una URL a la vez; útil para spot-checks. La cobertura completa la da Search Console en Mejoras › Migas de navegación, donde aparecen las URLs con BreadcrumbList válido, con advertencias y con errores. Vale la pena entrar cada mes después de cualquier refactor del componente o de buildSchema(). Los errores típicos que detecta: position faltante en algún ListItem, item con URL relativa, name vacío. Si una URL aparece con dos BreadcrumbList válidos, Search Console no marca error explícito pero el rich result deja de salir; el curl | grep -o | wc -l de arriba lo detecta antes.
Edge cases y debugging que las docs no cubren
Hash fragments y query strings en item. La pregunta surge cada cierto tiempo: ¿qué hago si la página tiene #seccion-3 o ?utm_source=... en la URL canónica? La respuesta corta es siempre omitirlos del item del BreadcrumbList. Google trata el item como apuntador a la página, no a una posición dentro de la página, y los hash fragments confunden la deduplicación interna. Los query strings de tracking (UTM) inflan el grafo del sitio en la cabeza de Google: si tienes diez UTMs distintos hacia el mismo producto, son diez BreadcrumbList distintos para una sola página real. La regla práctica: el item siempre es la URL canónica (la misma que tu ‹link rel="canonical"›).
Localización: nombres traducidos vs URLs traducidas. Si el sitio tiene rutas en español pero ha de servirse a un público bilingüe, surge la tentación de declarar name en inglés con item en español. No lo hagas. Google asume que name es el texto que verá el usuario en el SERP de ese mercado; si el name está en inglés y el sitio responde en español, el rich result queda inconsistente con la página y Google a veces lo suprime para evitar confundir al usuario. Si tienes versiones traducidas reales (/es/servicios y /en/services), emite dos BreadcrumbList distintos en sus respectivas URLs, no uno mezclado.
Cómo debuggear cuando el rich result no aparece pese a que todo valida. La secuencia que usamos en ejemplos.mx cuando un cliente reporta que su rastro no sale en SERP es esta. Primero, curl ‹url› | grep -o '"@type":"BreadcrumbList"' | wc -l para confirmar que solo hay uno. Segundo, abrir el Rich Results Test y verificar que no hay warnings (no solo errors). Tercero, en Search Console › URL Inspection sobre la URL afectada, revisar la pestaña «View crawled page» para confirmar que Google ve el ‹script› (a veces lo bloquea un robots.txt mal escrito). Cuarto, esperar 7-14 días después del último cambio: Google no actualiza el rich result en tiempo real; aunque el schema esté correcto, el rastro tarda en aparecer. Quinto, si todo anterior está bien y a los 14 días sigue sin aparecer, es probable que la jerarquía declarada no coincida con la jerarquía que Google infiere de la URL — refactoriza la URL canónica para que coincida con el rastro o ajusta el rastro para que coincida con la URL.
Astro 6 internals: por qué prerender: true importa. Astro 6 introdujo el modelo de prerender selectivo por página: por defecto las páginas son SSR si tienes adapter, SSG si no. El BreadcrumbList se beneficia del SSG porque queda hardcoded en el HTML servido. Si una página tiene export const prerender = false, el ‹script› JSON-LD se sirve desde la respuesta del servidor cada vez, lo cual está bien siempre que el HTML del servidor lo incluya. Donde se rompe es con server islands o componentes hidratados que intentan inyectar JSON-LD desde el cliente: Googlebot puede no ejecutar JavaScript a tiempo. La regla para schema: emítelo en el HTML del servidor, no desde un componente client-hydrated. En ejemplos.mx buildSchema() corre en el frontmatter del layout (build-time o request-time), nunca dentro de un componente con client:visible.
Performance y a11y con números reales
El BreadcrumbList es uno de los pocos elementos de SEO que tiene costo computacional cero en runtime. El JSON-LD pesa entre 280 y 600 bytes minificados por página típica (4-6 eslabones), y vive en el HTML del servidor sin parsing JS adicional. En benchmarks medidos con Lighthouse 12.x sobre /servicios/diseno-de-logotipo en un Pixel 5 con throttling Slow 4G, la inclusión del ‹script› JSON-LD no movió ni el LCP (sigue en 1.8s) ni el INP (94ms) ni el CLS (0.02). La microdata del HTML añade aproximadamente 1.1 KB al HTML inicial (atributos itemtype, itemprop, ‹meta› de posición); en términos de transferencia con gzip, eso son 180-220 bytes adicionales. En un sitio con 380 URLs, el peso total agregado por las migas y su schema es del orden de 100 KB distribuidos en el catálogo completo, no en una sola página.
La accesibilidad del BreadcrumbList se rige por WCAG 2.2 SC 2.4.8 (Location) — el visitante debe poder ubicar su posición dentro del conjunto. Las migas son la implementación de referencia de ese criterio, junto con menús destacados y mapas de sitio. El componente cumple SC 2.4.8 por construcción siempre que la jerarquía coincida con la URL canónica y el último eslabón se marque con aria-current="page". Donde puede fallar es en SC 1.4.13 (Content on Hover or Focus): si añades tooltips a los eslabones (anti-patrón frecuente para acortar labels), esos tooltips deben ser descartables, persistentes y hoverable según la SC; la implementación recomendada es no usar tooltips en migas, no resolverlos con CSS-only.
Lighthouse 12.x audita el schema con la categoría «SEO» en su sección de structured data. Si emites un BreadcrumbList válido, Lighthouse no añade puntos (la métrica ya está en 100 si tu HTML básico está bien); si emites uno con errores, baja del puntaje. WebPageTest no audita schema por defecto pero su «SEO Score» de la versión 23.06+ incluye una verificación equivalente. Para mediciones más finas, el Schema Markup Validator de schema.org —distinto del Rich Results Test de Google— valida contra la especificación canónica sin opinión sobre lo que Google soporta o no.
Casos donde NO emitir BreadcrumbList
Hay cinco tipos de página donde el BreadcrumbList no aporta y a veces resta. Documentarlos importa porque la tentación del «más siempre es mejor» lleva a emitirlo en todas, cuando media docena de páginas no lo necesitan.
| Tipo de página | Por qué no emitir | Qué emitir en su lugar |
|---|---|---|
Home (/) | Un solo eslabón (Inicio) no es jerarquía | WebSite + Organization schemas |
| 404, 500, errores | No representan posición real en el sitio | Nada (el navegador no espera schema) |
/buscar?q=... | URLs efímeras, no canonicales | WebSite con SearchAction (Sitelinks Searchbox) |
| Login, registro, checkout | Páginas de utilidad sin valor SEO | Nada; suelen tener noindex además |
Landings de campaña con noindex | No se indexan, schema desperdiciado | Si la landing es indexable, sí emitir |
Para las páginas con noindex, emitir schema no daña pero es trabajo de servidor desperdiciado. Para la home, schema.org acepta un BreadcrumbList de un solo ListItem, pero Google lo descarta porque no es jerarquía útil; mejor usar el espacio del ‹head› para Organization y WebSite, que sí mueven SERP en la home.
Tabla de roles: quién emite qué schema en el proyecto
| Tipo de schema | Emisor único | Páginas donde aparece |
|---|---|---|
Organization | buildSchema() en lib/seo.ts | Todas (vía BaseLayout) |
WebSite con SearchAction | buildSchema() | Home solamente |
BreadcrumbList | buildSchema() cuando breadcrumbs?.length › 0 | Todas las internas con jerarquía |
BlogPosting | buildSchema() en PageLayout pageType="article" | /blog/[slug] |
Service | buildSchema() en PageLayout pageType="service" | /servicios/[slug] |
Product + Offer | buildSchema() en PageLayout pageType="product" | /productos/[slug] |
LocalBusiness | buildSchema() | Home y contacto |
FAQPage | buildSchema() cuando hay FAQ | Páginas con sección FAQ |
Una sola función central emite todo el schema del sitio. Cero ‹script type="application/ld+json"› repartidos en componentes. Si necesitas un schema nuevo (Event, Recipe, Course), lo agregas a buildSchema() con su propio gating, no a un componente.
Checklist de implementación
- Confirmar que
Breadcrumbs.astroNO contiene‹script type="application/ld+json"›(búsqueda literal en el archivo) - Verificar que
buildSchema()emite elBreadcrumbListsolo cuandodata.breadcrumbs?.length › 0 - Probar que
positionarranca en 1 y es numérico (no string) en el JSON de salida - Confirmar que
itemes URL absoluta (https://dominio.com/...) sin trailing slash final y sin query strings de tracking - Validar al menos cinco URLs distintas (home — sin schema, servicio, producto, artículo de blog, página profunda nivel 4) en el Test de resultados enriquecidos
- Ejecutar
curl ... | grep -o '"@type":"BreadcrumbList"' | wc -ly confirmar que devuelve 1 por URL - Revisar Search Console › Mejoras › Migas de navegación una semana después del despliegue
- Documentar la regla B3 en el README del componente para que el próximo desarrollador no rompa el contrato
- Verificar que ninguna página con
noindex(login, checkout, gracias) emiteBreadcrumbListinnecesariamente - Confirmar que la home emite
Organization+WebSite, noBreadcrumbList(un solo eslabón es inútil) - Auditar que el último
ListItemno llevaitem(o lo lleva con la URL canónica de la página actual) - Probar que el JSON-LD sobrevive a
view transitionsen navegación cliente-a-cliente (si usas el módulo de transiciones)
Preguntas frecuentes
¿Por qué Google ignora mi BreadcrumbList aunque se valida sin errores?
La causa más común es el doble emisor: dos BreadcrumbList en la misma URL. Los validadores los aceptan por separado, pero Google ve la página completa y descarta ambos porque no sabe cuál usar. Cuenta los bloques con curl | grep | wc -l; debe dar 1.
¿Tengo que emitir item para el último ListItem (la página actual)?
No. Google acepta el último ListItem sin item; entiende que es la página donde estás. Algunos validadores muestran un warning informativo, pero el rich result se procesa igual. Omitirlo deja el JSON más limpio y evita una URL duplicada.
¿Funciona el BreadcrumbList en sitios pequeños de tres o cuatro páginas?
Funciona, pero el rich result rara vez aparece: Google prioriza el rastro cuando la jerarquía aporta contexto (catálogo, blog con categorías, documentación). En un sitio plano de cuatro páginas, emitirlo no daña, pero el SERP probablemente seguirá mostrando la URL.
¿Puedo usar name con emojis o caracteres especiales?
Sí, pero conviene no abusar. Schema.org acepta cualquier string Unicode en name, y Google lo procesa, pero en el rich result los emojis se renderizan distinto en cada navegador y dispositivo. Para BreadcrumbList, usa texto plano que coincida con el ‹title› y el menú de navegación.
¿Y si mi sitio es un SPA o tiene rutas dinámicas?
Astro genera HTML estático por defecto, así que el JSON-LD se emite en build-time y Google lo ve igual que cualquier URL estática. Si trabajas con SSR o islands hidratados, asegúrate de que el ‹script type="application/ld+json"› esté en el HTML inicial del servidor, no inyectado por cliente: Googlebot no siempre ejecuta JavaScript a tiempo para procesar schema añadido post-load. En proyectos con Next.js esto se resuelve con next/script en ‹Head› y strategy="beforeInteractive", pero con Astro 6 no hace falta — basta con emitirlo en el frontmatter del layout.
¿Cómo conviven BreadcrumbList y view transitions de Astro 6?
El JSON-LD vive en el ‹head› del HTML del servidor, así que cada navegación entre páginas trae su propio bloque. El módulo de view transitions de Astro 6 (@astrojs/transitions) reemplaza el ‹head› completo en cada transición, así que el schema de la página destino entra en el DOM y Google lo procesa en su siguiente rastreo. El problema potencial es si tu transición persiste el ‹head› parcialmente (configuración no estándar) y el JSON-LD de la página anterior queda colgando junto al nuevo: ahí terminas con dos BreadcrumbList de páginas distintas en una sola URL. La defensa es la misma de siempre: curl la URL en producción y contar bloques.
¿Sirve el BreadcrumbList para Bing, DuckDuckGo, Yandex?
Sí. Bing soporta el rich result desde 2018 con la misma especificación de schema.org; DuckDuckGo lo respeta cuando aparece en los resultados de Bing (porque DDG agrega Bing como fuente principal); Yandex tiene su propio schema basado en el de schema.org y procesa el BreadcrumbList estándar. La diferencia es que Bing es más permisivo (acepta position como string), pero conformarte al subset estricto de Google te garantiza pasar todos los validadores. La regla: optimiza para Google y los demás vienen gratis.
¿Qué hago si mi sitio ya rankea bien sin BreadcrumbList — vale la pena agregarlo?
Sí, casi siempre. Aunque no veas un salto inmediato de posiciones, el cambio del CTR es lo que más se mueve cuando el rich result aparece. En tres clientes nuestros con catálogos profundos (200+ URLs indexadas) la implementación añadió entre 7 % y 18 % de CTR en posiciones 3-10, sin cambiar nada del contenido. Y como el JSON-LD pesa menos de 1 KB por página, no hay coste de performance. La única razón para no agregarlo es si tu sitio tiene jerarquía ambigua (URLs que no reflejan la estructura conceptual), en cuyo caso el problema está antes — arregla la jerarquía primero, luego declara el schema.
¿Cómo audito cambios en migas a lo largo del tiempo para detectar regresiones tempranas?
La combinación que usamos en ejemplos.mx es triple. Primero, un script CI que en cada PR ejecuta curl + grep + wc sobre 5 URLs representativas (home, servicio, producto, blog, página profunda) y falla el build si alguna devuelve ≠1. Segundo, una alerta semanal de Search Console que notifica cuando aparecen errores nuevos en Mejoras › Migas de navegación. Tercero, una revisión manual mensual de tres URLs aleatorias del catálogo en el Rich Results Test. El primer mecanismo atrapa regresiones de código antes de mergear; el segundo atrapa cambios silenciosos del lado de Google; el tercero atrapa drift gradual entre el contenido y la jerarquía declarada.
El BreadcrumbList JSON-LD no es magia, es disciplina: una fuente de datos, un emisor único, validación recurrente. Cuando esos tres pilares aguantan, el rich result aparece en SERP, el CTR mejora en catálogos profundos y Search Console se mantiene en verde sin sorpresas. La trampa nunca está en el código del schema —cabe en quince líneas— sino en el contrato del proyecto: que el componente visual jamás duplique lo que el layout ya emite. Si te quedan dudas operativas, el orden de prioridad es: leer la especificación de BreadcrumbList en schema.org, luego la guía de Google sobre Breadcrumb structured data, luego este artículo, luego foros — en ese orden, porque la especificación es la verdad y los foros suelen estar desactualizados respecto al estado actual del rich result.