Guía · Topbar

Barra utilitaria superior (la franja por encima del menú). Cada punto se alimenta de src/config/site.ts (fuente única); no se escribe a mano. Esto es lo que va en cada lugar:

  1. 1

    Propuesta principal

    Lo primero que se lee: una frase corta que posiciona la marca. El logotipo NO va aquí — va en el Header, justo debajo.

    Dato SITE.tagline

  2. 2

    Horario

    Señal de disponibilidad y confianza. Se oculta en móvil para priorizar el teléfono y WhatsApp.

    Dato CONTACT.schedule.display

  3. 3

    Teléfono

    Contacto directo con clic-para-llamar. El enlace tel: lo construye telUrl(), no se escribe a mano.

    Dato CONTACT.phone · telUrl()

  4. 4

    WhatsApp

    CTA principal de contacto. El enlace SIEMPRE se arma con waUrl(); el mensaje precargado sale de WA_MESSAGES.

    Dato waUrl(WA_MESSAGES.cotizar)

Edita en src/components/TopBar.astro · src/config/site.ts

Guía · Header

Barra de navegación principal (logotipo + menú), bajo el topbar. Todo el menú —escritorio, paneles y móvil— se genera desde NAV en src/config/site.ts (fuente única); no se escribe a mano. Esto es lo que va en cada lugar:

  1. 1

    Logotipo

    La marca, a la izquierda, enlazando a la home. Es el ancla de identidad y el «volver al inicio» que todos esperan. Aquí SÍ va el logo (en el topbar no).

    Dato SITE.brand · SITE.name

  2. 2

    Navegación

    Las secciones del sitio. No se hardcodea ningún enlace: se itera NAV, la misma fuente para escritorio y móvil. En móvil colapsa en el menú ☰.

    Dato NAV

  3. 3

    Paneles (mega / dropdown)

    Las secciones con hijos despliegan un panel al pasar el cursor o con el teclado; su contenido sale de la taxonomía, no de una lista aparte.

    Dato NAV[].panel · items

  4. 4

    CTA · Cotizar

    El botón de conversión a WhatsApp, siempre visible a la derecha. El enlace se arma con waUrl(); el mensaje precargado sale de WA_MESSAGES.

    Dato waUrl(WA_MESSAGES.cotizacion)

Edita en src/components/Header.astro · src/config/site.ts

Guía · Migas de pan

La ruta que muestra dónde está el visitante dentro de la jerarquía del sitio, justo debajo del header. Sirve para dos cosas a la vez: orientar y dejar volver a cualquier nivel superior, y alimentar el BreadcrumbList de schema.org que el buscador usa para mostrar la ruta en sus resultados. Cada página define su ruta una sola vez con la prop breadcrumbs; el JSON-LD lo emite buildSchema (no este componente, para no duplicarlo). Esto es cada eslabón:

  1. 1

    Raíz (Inicio)

    El primer eslabón: siempre enlaza a la home. Es el punto de partida de la ruta y el «volver al inicio» que todos esperan de la jerarquía.

    Dato items[0] · href '/'

  2. 2

    Eslabón intermedio

    Cada nivel ancestro entre la home y la página actual (categoría, subcategoría). Son enlaces: dejan saltar a cualquier nivel superior.

    Dato items[].href

  3. 3

    Separador

    El icono (›) entre eslabones. Es decorativo —va con aria-hidden— y solo marca la dirección de la jerarquía; nunca es un enlace.

    Dato SVG · aria-hidden

  4. 4

    Página actual

    El último eslabón: la página donde estás. No enlaza (ya estás ahí) y se marca con aria-current="page" para los lectores de pantalla.

    Dato item sin href · aria-current

Edita en src/components/Breadcrumbs.astro · prop breadcrumbs de cada página

guias

Migas de pan en Astro: guía paso a paso

Cómo implementar migas de pan en Astro para fortalecer la navegación, evitar duplicar JSON-LD y mantener la jerarquía coherente en cada página.

Migas de pan en Astro: guía paso a paso

Las migas de pan parecen un componente trivial hasta que aparecen tres versiones distintas en el mismo sitio, una duplica el BreadcrumbList en el JSON-LD y otra olvida el aria-current que necesita el lector de pantalla. Esta guía arma el componente en Astro de principio a fin, con la anatomía de los cuatro eslabones, el contrato de la prop breadcrumbs por página y el reparto exacto entre microdata visible y JSON-LD central. Es para desarrolladores que construyen sitios con Content Collections y quieren un componente que orienta al visitante, se valida en Search Console y nunca duplica schema.

Contexto

El visitante de una página interna llega de buscador o de un enlace lateral, y la primera pregunta que se hace sin formularla es «¿dónde estoy parado dentro del sitio?». Una sola línea bajo el header con Inicio › Servicios › Diseño de logotipo responde esa pregunta en menos de un segundo. Sin ella, la única forma de ubicarse es abrir el menú, leer todas las categorías y deducir cuál contiene la página actual. La fricción que parece pequeña en un sitio de cinco páginas se vuelve abandono en un catálogo de cincuenta.

Las migas hacen dos trabajos al mismo tiempo. El primero es navegación visible: cada eslabón superior es un enlace de verdad, así el visitante salta a la categoría o a la home sin recurrir al botón «atrás» del navegador, que en escritorio rompe scroll y en móvil a veces ni siquiera está. El segundo es estructura para el buscador: el BreadcrumbList de schema.org le dice a Google la posición de la página dentro de la jerarquía. Cuando se cumplen los requisitos, ese rastro aparece bajo el título en los resultados en lugar de la URL cruda, que se lee fatal en dominio.com/servicios/categoria-x/sub-categoria-y/pagina-actual.

El error más caro al implementarlas viene de no decidir quién emite el JSON-LD. Si el componente visual lo emite y el layout también, terminas con dos BreadcrumbList en el ‹head› y Google ignora ambos. La regla dura del proyecto (B3) cierra el debate: el componente solo emite microdata HTML (itemtype, itemprop); el JSON-LD lo arma buildSchema() en lib/seo.ts, una sola vez por página, leyendo la misma prop breadcrumbs que recibe el componente. Una fuente, dos consumidores.

Implementación paso a paso

El componente vive en src/components/Breadcrumbs.astro y recibe una lista mínima: cada eslabón lleva label y, salvo el último, href. La página declara su rastro en la prop breadcrumbs del PageLayout, y este lo entrega tanto al componente visual como a buildSchema(). La página nunca toca microdata ni JSON-LD a mano.

---
// src/pages/servicios/diseno-de-logotipo.astro
// Cada página declara SU rastro UNA sola vez en la prop breadcrumbs.
// El componente antepone «Inicio» en el visual y buildSchema arma el JSON-LD.
import PageLayout from '@layouts/PageLayout.astro'
---

<PageLayout
  title="Diseño de logotipo — Servicios"
  description="…"
  pageType="service"
  breadcrumbs={[
    { label: 'Servicios', href: '/servicios' },
    { label: 'Diseño de logotipo' },        // sin href = página actual
  ]}
>
  {/* …contenido de la página… */}
</PageLayout>

Dentro del componente la lógica es muy chica. Acepta items y, si el primero no es la raíz, antepone Inicio automáticamente, así la página nunca repite la home. Esto vive en src/components/Breadcrumbs.astro:34-35:

---
// Si la página ya incluyó la raíz, no se duplica.
const trail: BreadcrumbItem[] =
  items[0]?.href === '/' ? items : [{ label: 'Inicio', href: '/' }, ...items]
---

El render real (sin modo guía) emite un ‹ol› con itemscope itemtype="https://schema.org/BreadcrumbList". Cada ‹li› es un ListItem con su position por ‹meta›, y el último eslabón —sin href— se marca con aria-current="page" para que los lectores de pantalla lo anuncien como destino actual. El separador es un SVG con aria-hidden="true"; no es enlace ni se lee. Esto está en Breadcrumbs.astro:97-111:

<nav aria-label="Migas de pan">
  <ol itemscope itemtype="https://schema.org/BreadcrumbList">

    <li itemprop="itemListElement" itemscope
        itemtype="https://schema.org/ListItem">
      <a href="/" itemprop="item"><span itemprop="name">Inicio</span></a>
      <meta itemprop="position" content="1" />
    </li>

    <svg aria-hidden="true" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>

    <li itemprop="itemListElement" itemscope
        itemtype="https://schema.org/ListItem">
      <span aria-current="page" itemprop="name">Diseño de logotipo</span>
      <meta itemprop="position" content="2" />
    </li>

  </ol>
</nav>

El detalle que se escapa es el ‹meta itemprop="position"›: schema.org requiere que cada ListItem declare su posición numérica empezando en 1. Sin eso, el BreadcrumbList se valida con advertencias y Google a veces no lo procesa. Va dentro del ‹li›, no fuera, y va con String(index + 1) porque el atributo content solo acepta string.

La barra visible se carga sola dentro del PageLayout. No hay que importar Breadcrumbs.astro en cada página: el layout lo monta justo debajo del header, antes del hero, en cuanto detecta la prop breadcrumbs. La página solo declara la ruta.

Tabla comparativa

Variante visualCuándo usarlaTrade-off
Texto con separador (›)Sitio de negocio, servicios, blogLa más segura, universal, ojo entrenado; no aporta peso táctil
Icono home en la raízE-commerce con catálogo grandeCompacta la línea, refuerza «volver al inicio»; pierde la palabra «Inicio»
Cápsulas (pills)SaaS, paneles operativosBlanco táctil grande, jerarquía como información; agrega ruido visual
Atrás + actual (móvil)Detalle de producto en móvilMantiene camino de vuelta en una línea; pierde el rastro completo
Colapsadas con «…»Catálogo profundo, wikiCabe en una línea con cinco niveles; un toque extra para ver intermedios
Truncado con tooltipDocumentación, CMSTítulos largos sin multilínea; el tooltip no es accesible en touch

La decisión por contexto importa más que el aspecto. Un sitio de servicios con tres niveles de profundidad nunca necesita colapsar nada; un wiki con seis niveles colapsa siempre. La trampa es elegir la variante por estética: si te suena bonito poner pills en un sitio de despacho legal, en seis meses las migas pesarán más visualmente que el menú principal.

Patrones avanzados

Microdata visible coexistiendo con JSON-LD central. El componente lleva itemtype/itemprop en el HTML porque cuesta cero y suma una segunda señal estructurada para los rastreadores que no parsean JSON-LD (algunos motores antiguos, scrapers de redes sociales). El JSON-LD vive aparte, en el ‹head›, emitido por buildSchema(). Las dos representaciones leen exactamente la misma prop breadcrumbs, así que no se desincronizan. Si modificas la jerarquía en un lugar, cambia en los dos al mismo tiempo. La regla práctica: una sola fuente de datos (la prop), varios formatos de salida.

Accesibilidad real, no de checklist. El aria-label="Migas de pan" en el ‹nav› deja que los lectores de pantalla anuncien el landmark; sin él, el ‹nav› se confunde con el menú principal. El aria-current="page" en el último eslabón le dice al lector «esta es la página actual», y el separador SVG con aria-hidden="true" evita que el lector deletree chevron right entre cada palabra. Si añades hover effects, asegúrate de que el :focus-visible del enlace sea igual de claro que el :hover: la navegación por teclado debe ver el mismo highlight que el cursor.

Scroll horizontal en móvil sin partir la línea. En pantallas chicas, una ruta de tres o cuatro niveles se parte en dos renglones y empuja el hero hacia abajo. La solución sin sacrificar el rastro completo es flex-wrap: nowrap + overflow-x: auto + ocultar la barra de scroll:

<style>
  .breadcrumb__list {
    display: flex;
    flex-wrap: nowrap;                      /* no multilínea */
    overflow-x: auto;
    scrollbar-width: none;                  /* Firefox */
    -webkit-overflow-scrolling: touch;      /* inercia iOS */
  }
  .breadcrumb__list::-webkit-scrollbar { display: none; }
</style>

El resultado se siente como una app: la ruta se desliza con el dedo, sin línea gris, y la página actual queda visible al final del scroll para que el visitante sepa dónde está. La alternativa popular —ocultar las migas en móvil con display: none— pierde el rastro y suele empeorar el SEO porque Google rastrea el HTML completo.

Checklist de implementación

  • Declarar la prop breadcrumbs en cada PageLayout de página interna (no en la home)
  • Confirmar que el último eslabón va sin href y se renderiza con aria-current="page"
  • Verificar que Breadcrumbs.astro NO emite ningún ‹script type="application/ld+json"›
  • Confirmar que buildSchema() emite el BreadcrumbList una sola vez por página
  • Probar la barra con tecla Tab: cada eslabón debe tener foco visible y el separador debe saltarse
  • Validar dos URLs en el Test de resultados enriquecidos sin advertencias
  • Revisar en móvil real (no DevTools): la ruta debe scrollear horizontalmente sin multilínea
  • Confirmar etiquetas cortas: «Servicios» no «Nuestros servicios profesionales para empresas»

Preguntas frecuentes

¿Debo poner migas de pan en la home?

No. En la home el visitante ya está en la raíz: las migas serían una línea que dice solo Inicio y eso es ruido. La regla del componente lo refleja: las páginas que no declaran la prop breadcrumbs no pintan la barra.

¿Y si mi página tiene varios padres lógicos (un producto en dos categorías)?

Elige una sola jerarquía canónica por URL. Si el producto vive en /productos/audio/auriculares-x, las migas siguen esa ruta; el otro acceso (por marca, por uso) se resuelve con enlaces internos en el cuerpo, no con un segundo BreadcrumbList. Google solo admite una jerarquía por página.

¿Puedo omitir el JSON-LD si ya tengo microdata visible?

Puedes, pero pierdes el rich result. Los datos estructurados de Google priorizan JSON-LD para BreadcrumbList; la microdata visible te da una segunda señal y refuerza la semántica del HTML, pero por sí sola rara vez dispara el rastro bajo el título en los resultados.

¿Las migas reemplazan al menú principal?

No. El menú es navegación lateral (categorías hermanas, secciones del sitio); las migas son navegación jerárquica (ancestros de esta página). Un visitante en Servicios › Diseño de logotipo usa las migas para volver a Servicios, y el menú para saltar a Productos. Si las confundes, terminas con dos componentes que dicen lo mismo.

¿Tengo que actualizar las migas cuando renombro una categoría?

Si tu rastro nace de la URL o de una taxonomía central, no: el cambio se propaga solo. Si las hardcodeaste en cada página (anti-patrón D3), sí, y vas a olvidar alguna. La plantilla las declara por página, pero las etiquetas suelen venir de la misma fuente que el menú, así que un solo cambio en site.ts repinta todas.

Las migas bien hechas son una de esas piezas que el visitante deja de ver porque siempre están ahí, en su sitio, sin estorbar. Un componente de cincuenta líneas que ahorra clicks, mejora el rich result y se valida sin advertencias en Search Console. El truco no está en el código —es trivial— sino en la disciplina de no emitir el JSON-LD dos veces y en mantener una sola fuente para ambos consumidores.

Sigue leyendo

¿Listo para dar el siguiente paso?

Cuéntanos qué necesitas y te respondemos hoy mismo.

¿Necesitas ayuda?