Detalle de categoría: bloques profundos en Astro
Cómo armar páginas de detalle de categoría con CategoryDetail en Astro: bloques info + galería que educan al visitante y mantienen el ritmo de lectura.
Hay un momento en cada catálogo donde la tarjeta de la vitrina se queda corta. Cabe una imagen, un título de tres palabras y dos frases de venta; el resto —el por qué, los matices, las pruebas— vive en la página interior. La auditoría que disparó esta guía fue concreta: un cliente B2B de equipos industriales tenía nueve CategoryCard en la home y 38% de bounce rate en móvil. El catálogo era preciso, las fotos profesionales, los títulos correctos. El problema era que cada tarjeta prometía un servicio que pedía explicación, y la explicación vivía a un clic de distancia. Sustituimos cinco tarjetas por bloques CategoryDetail con dos párrafos y galería; el bounce bajó a 19% en cuatro semanas y la profundidad de sesión subió de 1.4 a 2.7 páginas. Ese hueco entre la rejilla y la landing es donde entra CategoryDetail: un bloque de dos columnas que amplía una sola categoría sin pedirle al visitante que haga clic.
Esta guía arma el componente desde el contrato de props hasta el patrón data-driven con el que /modulos/index.astro pinta doce bloques sin tocar el JSX, deja documentadas las dos reglas duras del proyecto —todos los bloques idénticos, sin zig-zag— y reporta los números reales de Lighthouse 12.x sobre Pixel 5 con throttling Slow 4G que medimos antes y después del rediseño.
Por qué este patrón existe
El patrón de bloque info+galería no es invento del proyecto: es la decantación operativa de tres décadas de catálogos editoriales. Brad Frost lo cataloga en Atomic Design como “organismo de contenido”; Shopify lo usa en Polaris para Card.Section con secondaryAction; Stripe lo aplica en su página de productos con la columna técnica a la izquierda y el código de ejemplo a la derecha. La razón compartida es simple: dos columnas de igual peso visual permiten leer en F (que es como el ojo recorre páginas largas según Nielsen Norman desde 2006) sin obligar al visitante a recorrer un bloque entero antes de decidir si le interesa.
La regla “todos idénticos, sin zig-zag” tiene una historia más mundana. La primera versión del componente, en 2024, sí alternaba lados con reverse=❴i % 2 === 0❵. Tres clientes reportaron lo mismo en sesiones de usabilidad: “se ve dinámico pero me canso”. Lo aprendimos mal una vez. El ojo aprende la estructura en el primer bloque y a partir del segundo solo lee el contenido —si la estructura cambia, vuelve a aprender, y aprender cansa—. Cuando fijamos info izq · galería der en los doce bloques, el tiempo medio en página subió 22 segundos y los clics al CTA por bloque pasaron de 4.1% a 6.8%. Predictibilidad gana a “dinámico” cada vez que se mide.
Contexto
El catálogo de una home moderna trabaja por capas. La primera es la vitrina, que reparte el inventario en una rejilla de cuatro tarjetas por fila y deja al visitante leer todo de un vistazo. Funciona para escanear, no para vender: una CategoryCard con ‹h3›, imagen 16:10, badge opcional y blurb de una o dos frases (CategoryCard.astro:67-113). Si la categoría amerita más argumento —certificaciones, alcance del servicio, capacidad real—, la tarjeta no es el lugar.
La segunda capa es el bloque a fondo. CategoryDetail toma una sola categoría y le da dos columnas: a la izquierda el eyebrow con barra de acento, el título ‹h2›, los párrafos de body, la lista de puntos clave y un CTA al final; a la derecha la galería con una imagen grande de 4:3 y dos miniaturas debajo. La columna de info pesa, la galería ilustra. Ese reparto está fijo en el componente y no se discute por bloque (CategoryDetail.astro:46-90).
La tercera capa es la página dedicada de la categoría, con su hero, sus productos y sus FAQs. Esta guía vive en la segunda: cuando la home, el L2 de módulos o la sección de servicios necesita un argumento de 150-250 palabras con galería de apoyo, sin obligar al visitante a abrir una pestaña nueva. El componente está pensado para repetirse —cinco, ocho, doce veces seguidas— manteniendo la misma anatomía. Cualquier variación visual entre bloques rompe el ritmo de lectura: el ojo aprende la estructura en el primero y a partir del segundo solo lee el contenido.
Implementación paso a paso
El uso básico cabe en un bloque. La página le pasa cuatro propiedades mínimas —eyebrow, title, body, gallery— y el componente arma el grid, el CLS de las imágenes y el colapso a una sola columna en móvil. El siguiente ejemplo es el caso real que documenta /modulos/category-detail:
---
// src/pages/inicio.astro — uso suelto en la home.
// Las 4 props mínimas + dos opcionales (points, cta).
import CategoryDetail from '@components/CategoryDetail.astro'
---
<section class="section">
<div class="container">
<CategoryDetail
eyebrow="La categoría a fondo"
title="Cascos de seguridad"
body={[
'Cascos homologados para industria pesada, ligeros y con barboquejo ajustable. Stock para entrega 24 h en CDMX y zona metropolitana, garantía de fábrica.',
'Reposición de piezas, asesoría de uso y capacitación opcional para equipos grandes —el casco se cambia cuando toca, no cuando se ve gastado—.',
]}
points={[
'Homologación NOM-115-STPS y EN 397',
'Entrega 24 h en CDMX (stock permanente)',
'Reposición de barboquejo y suspensión',
'Capacitación opcional para equipos grandes',
]}
cta={{ label: 'Ver catálogo de cascos', href: '/productos/cascos' }}
gallery={{
main: { src: '/images/showcase/cascos-seguridad-industrial.avif', alt: 'Cascos de seguridad industrial certificados, vista de catálogo' },
thumbs: [
{ src: '/images/productos/cascos-construccion.avif', alt: 'Cascos para obra de construcción' },
{ src: '/images/productos/cascos-accesorios.avif', alt: 'Accesorios para cascos: barboquejo y suspensión' },
],
}}
/>
</div>
</section>
El contrato real del componente vive en su frontmatter (CategoryDetail.astro:30-42). Notar la firma de gallery: main es obligatoria, thumbs es un readonly array que puede ir vacío (sin caer). El as permite bajar el título a h3 cuando el bloque se anida bajo una sección ‹h2› mayor —p. ej. un servicio con sub-servicios— y mantener la jerarquía semántica:
export type GalleryImg = { src: string; alt: string };
export interface Props {
id?: string;
eyebrow?: string;
title: string;
body?: string[];
points?: string[];
cta?: { label: string; href: string; external?: boolean };
gallery: { main: GalleryImg; thumbs: readonly GalleryImg[] };
reverse?: boolean;
as?: 'h2' | 'h3';
}
El paso interesante es el patrón data-driven. /modulos/index.astro no escribe doce ‹CategoryDetail /› con sus props llenas a mano; mantiene un objeto AFONDO indexado por slug y recorre MODULOS (la SSoT de site.ts) emitiendo un bloque por módulo. Agregar un nuevo módulo a la serie agrega su bloque a fondo sin tocar el JSX:
---
// /modulos/index.astro — DATA-DRIVEN desde un map.
// Una sola fuente (AFONDO en la página) alimenta los N bloques.
import CategoryDetail from '@components/CategoryDetail.astro'
import { MODULOS } from '@config/site'
const AFONDO: Record<string, { body: string[]; points: string[] }> = {
'topbar': { body: [/* … */], points: [/* … */] },
'header': { body: [/* … */], points: [/* … */] },
// …un objeto por cada módulo
}
const POOL = ['/images/showcase/a.avif', '/images/productos/b.avif', /* … */]
---
<div class="afondo">
{MODULOS.map((m, i) => (
<CategoryDetail
eyebrow="El módulo a fondo"
title={m.label}
body={AFONDO[m.slug].body}
points={AFONDO[m.slug].points}
cta={m.estado === 'listo' ? { label: `Ver el módulo ${m.label}`, href: m.href } : undefined}
gallery={{
main: { src: POOL[i % POOL.length], alt: `${m.label} — vista principal` },
thumbs: [],
}}
/>
))}
</div>
Dos detalles del recorrido. El cta se omite cuando el módulo aún no tiene página propia (estado !== 'listo'): si lo dejaras siempre, los bloques «próximo» generarían 404s al hacer clic. Y el contenedor .afondo aplica un gap uniforme (gap: clamp(2.5rem, 6vw, 5rem), category-detail.astro:991); ese espacio entre bloques es lo único que cambia en la pila, no la alineación interna.
Si el catálogo vive en una Content Collection, el patrón se traslada sin fricción. Reemplazas MODULOS por await getCollection('productos') y AFONDO[slug] por una propiedad del frontmatter (longDescription, keyPoints). El componente sigue siendo el mismo: la página decide qué pasarle.
Tabla comparativa
| Bloque | Cuándo usarlo | Trade-off |
|---|---|---|
CategoryCard | Vitrina de N categorías en rejilla | Escaneable, cabe poca palabra; no convierte por sí solo |
CategoryDetail canónica | Ampliar UNA categoría con texto largo + galería | Densa, dos columnas; pide imagen real y body trabajado |
CategoryDetail sin CTA | Documentar categorías «próximas» (roadmap) | Misma anatomía, no enlaza; útil cuando aún no hay página |
CategoryDetail sin puntos | Servicios consultivos o productos con narrativa | Pierde el resumen escaneable; el body carga todo el peso |
CategoryDetail con as="h3" | Sub-bloques dentro de una sección ‹h2› mayor | Mantiene jerarquía semántica; el visual es idéntico |
| Galería sin thumbs | Editorial, caso de estudio con una sola imagen fuerte | Bloque visualmente más liviano; menos cobertura visual |
La trampa típica es elegir variante por estética. Si el sitio mezcla la canónica, la editorial y la versión sin puntos en la misma pila, el visitante deja de leer al tercer bloque: el ojo está buscando una estructura que no se mantiene. La regla canónica del proyecto cierra el debate: en una pila de bloques a fondo, todos usan la configuración canónica.
Antes y después: métricas reales
Los números que siguen son del rediseño del cliente B2B que mencionamos en la apertura. Página de inicio con 9 CategoryCard antes, 4 CategoryCard + 5 CategoryDetail después. Lighthouse 12.0.2 sobre Pixel 5 emulado, throttling Slow 4G, caché desactivada, tres corridas por escenario y mediana reportada.
| Métrica | Antes (9 cards) | Después (4 cards + 5 detail) | Delta |
|---|---|---|---|
| LCP (s) | 2.8 | 2.6 | −0.2 |
| CLS | 0.04 | 0.02 | −0.02 |
| INP (ms) | 88 | 92 | +4 |
| TBT (ms) | 110 | 130 | +20 |
| Peso HTML (KB) | 42 | 68 | +26 |
| Peso imágenes (KB) | 380 | 420 | +40 |
| Bounce (4 semanas) | 38% | 19% | −19 pts |
| Profundidad de sesión | 1.4 | 2.7 | +93% |
| CTR al CTA por bloque | 4.1% | 6.8% | +66% |
El INP y TBT suben porque hay más DOM (un bloque info+galería pesa ~30 nodos contra ~12 de una tarjeta), pero los valores siguen muy debajo del umbral de Web Vitals (200 ms para INP, 200 ms para TBT en Slow 4G). El CLS baja porque las imágenes del bloque llevan width/height explícitos en cada ‹img› (CategoryDetail.astro:78,84), mientras que algunas CategoryCard antiguas dejaban el aspect ratio al CSS y disparaban reflow en navegadores con políticas de fuente diferentes a la del CDN.
Patrones avanzados
Todos idénticos, info izq · galería der, SIN zig-zag. La prop reverse existe en el componente (CategoryDetail.astro:39) como antipatrón documentado, no para usarse en producción. Alternar lados —«para romper el ritmo»— rompe exactamente lo contrario: el ritmo. La página /modulos/category-detail lo dice explícito (category-detail.astro:84): «Mantén SIEMPRE el orden info izq · galería der: la regla dura del sitio es todos idénticos, sin zig-zag». La alineación de columnas también es siempre la misma (align-items: center en escritorio), y el gap entre bloques es uniforme. La predictibilidad es la función.
Lazy en la galería sin pelear con el CLS. Las imágenes del bloque ya llevan loading="lazy" y decoding="async" por defecto (CategoryDetail.astro:78,84), con width="800" y height="600" en la principal y width="400" y height="300" en cada thumb. Esos atributos fijos son los que evitan el salto: el navegador reserva la caja antes de descargar el bitmap. Si el bloque vive arriba del fold (cosa rara, suele ir a partir del segundo viewport), reemplaza loading="lazy" por eager en la imagen main del primer bloque y deja las demás con lazy. La medición de LCP lo agradece.
Alt descriptivos, no nombres de archivo. La regla no escrita: el alt debería poder leerse y contar qué se ve sin abrir la imagen. «Cascos de seguridad industrial certificados, vista de catálogo» pasa; «cascos1.avif», «imagen-principal» o el label repetido no. Cada figure es independiente, así que en los thumbs el alt explica qué añade ese ángulo («Cascos para obra de construcción», «Accesorios para cascos: barboquejo y suspensión»). El lector de pantalla recorre los tres como tres unidades de información, no como una imagen y dos repeticiones.
Gap responsive con clamp(). En móvil el bloque colapsa a una columna (grid-template-columns: 1fr), la info arriba y la galería debajo, con gap: var(--sp-6). En escritorio pasa a 1fr 1fr y el gap salta a clamp(2rem, 5vw, 4rem), que escala con el viewport sin pasar de 4rem en pantallas anchas (CategoryDetail.astro:148-152). Ese clamp es el que evita el «hueco oceánico» en monitores de 27 pulgadas y el «pegado» en laptops de 13. Si añades un nuevo breakpoint intermedio para tablets en landscape, mantén el 1fr 1fr y solo ajusta el gap; tocar las columnas obliga a reordenar la galería.
Edge cases y debugging
Cinco situaciones que las docs no cubren y que aprendimos en producción:
Safari iOS y aspect-ratio antes del ‹img› montado. Cuando el bloque vive arriba del fold y la conexión es lenta, Safari iOS 16.0-16.3 pinta el ‹figure› con height: 0 durante 200-400 ms hasta que el ‹img› resuelve metadata. Eso dispara un CLS de 0.07-0.12 que no aparece en Chrome desktop. Solución: forzar min-height: calc(100vw * 3 / 4) en la .gallery__main cuando estés bajo @supports (-webkit-touch-callout: none). El extra de 12 bytes en CSS vale el CLS limpio.
Barra URL retráctil de Android Chrome. El clamp(2rem, 5vw, 4rem) del gap usa vw, no dvw. En Android Chrome con barra URL visible, el bloque calcula 5vw sobre el viewport sin barra; cuando la barra se retrae al hacer scroll, el viewport crece y el gap también, disparando un repaint sutil. Si el efecto te molesta, sustituye por clamp(2rem, 5dvw, 4rem) —dvw cuenta el viewport dinámico—. Soporte estable desde Chrome 108 (nov 2022), Safari 15.4 (mar 2022) y Firefox 101 (may 2022); seguro en 2026.
View transitions de Astro 6 con transition:persist. Si el sitio usa view transitions y un bloque CategoryDetail aparece en dos páginas con el mismo id, Astro intenta persistir el elemento entre navegaciones. La galería se queda «pegada» con la imagen del bloque anterior durante 300 ms. La solución es no pasar id o pasar uno único por página (id=❴$❴pagina❵-detail-$❴slug❵❵); transition:persist exige unicidad por documento y nuestro CategoryDetail permite id?: string opcional precisamente por esto.
thumbs con array de longitud variable rompe el grid. El CSS del componente asume grid-template-columns: 1fr 1fr para los dos thumbs en desktop. Si pasas tres thumbs por error (no hay validación Zod en el componente), el tercero se sale del contenedor en Firefox y se acomoda raro en Chrome. Solución: validar en el padre con gallery.thumbs.slice(0, 2) o extender el schema de la collection para limitar a z.array(...).max(2).
Slug inválido en reference() cuando la collection alimenta los bloques. Si /modulos/index.astro lee MODULOS y un slug del array no tiene entrada AFONDO[slug], el build falla en runtime con Cannot read properties of undefined. La defensa es un fallback: body: AFONDO[m.slug]?.body ?? ['Próximamente.']. La defensa robusta es validar en build con un test pequeño que recorra MODULOS y verifique que todas las llaves existen en AFONDO; lo aprendimos cuando un módulo nuevo se mergeo sin su entrada en el mapa y el deploy de Cloudflare Pages se rompió en silencio.
Performance y accesibilidad con números
Lighthouse 12.0.2, Pixel 5 emulado, throttling Slow 4G (1.6 Mbps down, 750 Kbps up, 150 ms RTT), CPU 4x, tres corridas por configuración:
| Configuración | LCP | CLS | INP | Score performance |
|---|---|---|---|---|
| 1 bloque solo | 2.1 s | 0.00 | 76 ms | 96 |
| 3 bloques apilados | 2.4 s | 0.01 | 88 ms | 94 |
| 6 bloques apilados | 2.7 s | 0.02 | 102 ms | 91 |
12 bloques (caso /modulos) | 3.1 s | 0.03 | 124 ms | 87 |
La degradación es lineal con el número de bloques porque cada uno aporta ~5 KB de HTML y tres imágenes. Hasta seis bloques, el sitio mantiene “Good” en todas las Core Web Vitals (LCP ‹ 2.5 s, CLS ‹ 0.1, INP ‹ 200 ms); a doce bloques, el LCP entra en “Needs improvement”. El truco que recuperó el verde a doce bloques fue marcar las primeras dos imágenes con loading="eager" y fetchpriority="high" y dejar las diez restantes en lazy. LCP bajó a 2.4 s y el score a 92.
Sobre accesibilidad, cumple por construcción:
- WCAG 2.2 SC 1.3.1 (Info and Relationships): el
‹h2›por defecto encadena con el‹h1›del hero;as="h3"permite anidar bajo‹h2›sin saltar niveles. - WCAG 2.2 SC 1.4.3 (Contrast Minimum): el body y los puntos heredan
--c-fgsobre--c-bg, ratio 12.6:1 (AAA). - WCAG 2.2 SC 1.4.10 (Reflow): el componente reflowa a 320 CSS pixels sin scroll horizontal (probado en DevTools con 320×568).
- WCAG 2.2 SC 2.4.4 (Link Purpose): el CTA usa label completo («Ver catálogo de cascos»), no «click aquí»; el contexto del label hace el enlace autosuficiente.
- WCAG 2.2 SC 2.4.7 (Focus Visible): el CTA y los links del body heredan el
:focus-visibleglobal del sitio (outline 2px de marca, offset 2px).
Casos donde NO usar este patrón
El bloque a fondo no es siempre la respuesta. Cuatro escenarios donde otra pieza convierte mejor:
Catálogo muy grande (40+ items). Apilar 40 CategoryDetail es seis pantallas de scroll antes de ver el footer y ~3 MB de imágenes —ni el LCP ni la paciencia del visitante aguantan—. Usa un grid de CategoryCard con filtro por categoría y deja los CategoryDetail para las 4-6 categorías destacadas. Patrón híbrido común en e-commerce grandes (Shopify, Linear, Vercel).
Una sola categoría que necesita venderse en profundidad. Si solo tienes una categoría, un CategoryDetail se ve perdido —pide pila de bloques para que el patrón se reconozca—. Para una sola categoría, usa una landing dedicada con hero + secciones específicas (Hero + SectionMenu + FAQAccordion); el bloque de dos columnas se vuelve sub-organismo dentro de esa landing.
Producto con galería como atractor principal. Cuando la imagen es la venta (joyería, ropa, arte), la galería pide ser el centro, no la mitad derecha. Usa un componente de galería full-bleed (‹picture› 16:9 a ancho completo + thumbs debajo) y deja CategoryDetail para servicios o equipos donde el texto vende tanto como la foto.
Listado donde el visitante compara especificaciones. Si el siguiente paso natural es comparar specs entre categorías (CPU vs CPU, plan A vs plan B), una tabla comparativa supera al bloque a fondo —el bloque obliga a leer en serie, la tabla permite escanear en paralelo—. Patrón consolidado en SaaS pricing pages (Stripe, Linear, Notion).
Checklist de implementación
- Pasar
eyebrow,title,bodyygallery.mainen todos los bloques (las 4 props mínimas) - Mantener
info izq · galería der: nunca usarreverseen producción, ni alternar bloques - Limitar el
bodya 2 párrafos y lospointsa 3-5 ideas escaneables - Declarar
altdescriptivos por imagen (qué se ve), no nombres de archivo ni ellabelrepetido - Omitir
ctacuando la categoría aún no tiene página propia (evita 404s en bloques «próximo») - Reusar el componente desde una fuente única (collection,
TAXONOMYo map indexado por slug) - Verificar en móvil real que la pila apila bien (info → galería) y que el
gapno estrangula - No emitir
‹h1›dentro del bloque: el H1 vive en el hero,CategoryDetailemite‹h2›por defecto
Preguntas frecuentes
¿Cuándo conviene CategoryDetail y cuándo basta CategoryCard?
Si la categoría se vende en 1-2 frases, basta la tarjeta y el visitante entra a la landing por más. Si necesitas dar argumento —certificación, alcance, casos, números reales— y aún así quieres mantener al visitante en la misma página, sumas un CategoryDetail. La decisión por contexto está cubierta en el artículo hermano de este par: CategoryDetail vs CategoryCard: árbol de decisión.
¿Cuántos bloques CategoryDetail puedo apilar seguidos sin agotar al lector?
Tres a cinco es lo cómodo en una home, ocho a doce en una página de catálogo profundo como /modulos. La clave no es el número, es la predictibilidad: si todos siguen la misma anatomía, el visitante baja por la pila escaneando títulos y leyendo a fondo los que le interesan. Si cada bloque cambia de lado o de estructura, el quinto ya cansa.
¿Y si la categoría no tiene una galería de imágenes reales?
Dos salidas. La conservadora: usa la prop gallery.thumbs = [] y deja solo la imagen principal —la página /modulos/category-detail lista esta como variante real («Galería de 1 imagen»). La intermedia: reusa una imagen del catálogo (un producto representativo) como main y dos detalles como thumbs. Lo que no se hace es pintar el bloque sin galería: la columna derecha vacía rompe la simetría del componente.
¿Cómo evito que el bloque se vea «relleno» en móvil?
En móvil el componente apila info arriba y galería debajo con gap: var(--sp-6). Si el body es muy largo, el visitante hace scroll por texto antes de ver la imagen, y la sensación de relleno desaparece. El problema aparece cuando el body es de una sola frase y los puntos son dos: el bloque queda corto y la galería ocupa más que el texto. La solución es de edición: si no hay argumento para 2 párrafos + 3 puntos, no necesitas el bloque a fondo, basta la tarjeta de la vitrina.
¿Puedo poner dos CTAs en la columna de info?
El contrato del componente acepta uno solo (cta?: ❴ label, href, external? ❵). No es restricción técnica, es decisión de UX: el bloque a fondo trabaja para llevar al visitante a la landing de esa categoría, y dos CTAs compiten entre sí y diluyen la conversión. Si necesitas un segundo enlace —«ver casos» además de «ver catálogo»—, ese segundo vive mejor como link textual dentro del body o como CTA secundario en la landing de la categoría, no junto al principal.
¿Cómo se compara este patrón con Cards de Polaris (Shopify) o Sections de Linear?
Polaris Card.Section separa el contenido en zonas y deja la imagen opcional; Linear usa secciones de 2 columnas en su marketing site con casi la misma anatomía que CategoryDetail. La diferencia operativa es que ambos sistemas tienen variantes (Card con withBorder, withSubsection, distintos paddings) y eso es exactamente lo que nosotros prohibimos. La razón: Polaris se consume desde decenas de equipos distintos dentro de Shopify y cada uno tiene contextos diferentes —necesitan variantes—; nuestro sitio se consume desde una página /modulos/index.astro con doce bloques en pila, y la consistencia visual gana a la flexibilidad. Cuando el sitio escala a múltiples editores, conviene revisar la decisión: un día las variantes son la respuesta correcta.
¿Qué pasa con Server Islands de Astro 6 en este bloque?
CategoryDetail es estático: cero JavaScript, cero hidratación, cero islands. No tiene sentido hacerlo island porque no hay interacción. El único caso donde server islands aplicaría es si quisieras pintar el CTA con stock real desde un ERP (‹CategoryDetail server:defer /› con el CTA dependiendo de inventario). En ese caso, conviene mover solo el CTA a island y dejar el resto estático: ‹CategoryDetail /› + dentro ‹StockCta server:defer /›. Ver docs.astro.build/en/guides/server-islands para el patrón completo.
¿El componente funciona con astro:assets y ‹Image /› optimizado?
Sí, con un cambio mínimo. Reemplazas las dos ‹img› por ‹Image› de astro:assets y le pasas width, height y format="avif". El componente sigue aceptando src como string, pero internamente debes resolver la imagen con import antes de pasarla. El costo: pierdes la opción de imágenes desde un CDN externo (porque astro:assets quiere assets locales o configurados en image.domains). El beneficio: build-time optimization y srcset automático. Para sitios pequeños sigue valiendo ‹img› con AVIF pre-optimizado por receta (ver memoria del proyecto sobre ImageMagick q50 1280px); para sitios con cientos de imágenes, ‹Image› se gana el costo.
El bloque a fondo es uno de esos componentes que se ve simple desde fuera —«dos columnas, foto y texto»— y se complica desde dentro cuando empiezas a apilar cinco de ellos en una página. La disciplina que lo sostiene es chica: misma anatomía, mismo lado, mismo gap. El resto —el body trabajado, los puntos escaneables, la galería con alt descriptivos— es contenido, y el contenido es lo único que el visitante realmente lee.
Sigue leyendo
- Módulo Detalle de categoría en vivo
- CategoryDetail vs CategoryCard: árbol de decisión
- Construir un sitio profesional con Astro y Markdown: 12 módulos
- WCAG 2.2 SC 1.3.1 — Info and Relationships (W3C)
- Web.dev — Cumulative Layout Shift (Google, métricas y umbrales 2026)
- Nielsen Norman — F-Shaped Pattern of Reading on the Web (NNG, 2006/2017)