Guía de productos · Las imágenes

Las imágenes: entra por los ojos, sin pesar

La foto de un producto es obligatoria y se valida con una regex; el formato es AVIF (ligero); el alt describe con la palabra clave; y el montaje reserva el hueco (cero CLS) y cuida el LCP. Entra por los ojos sin arrastrar la carga.

Es la tercera decisión al crear un producto: cómo se ve. Una foto vende más que un párrafo, pero mal montada es justo lo que hunde el rendimiento —imágenes pesadas que tardan, fotos sin dimensiones que hacen saltar la maqueta, alt vacíos que dejan ciega a media audiencia—. Esta pieza monta la imagen para que sume sin penalizar.

El sistema pone las garantías de fábrica: la ruta se valida en build (no hay foto rota en producción), el formato es AVIF (peso mínimo), las dimensiones son fijas (cero saltos) y la carga es inteligente (la primera con prioridad para el LCP, el resto en diferido). Tú solo eliges una buena foto y escribes un buen alt.

Definición

¿Qué es la imagen de un producto?

El campo image del frontmatter (y la gallery opcional): una ruta absoluta bajo /images/, validada por Zod, que apunta a una foto AVIF. La card del catálogo y la ficha la montan con dimensiones fijas y carga según su posición.

La imagen es el campo image del frontmatter: una ruta a una foto que vive en /public/images/ y se sirve desde la raíz del sitio (/images/...). No es opcional —Zod la exige— y no admite cualquier valor: una regex obliga a que sea una ruta absoluta bajo /images/. La gallery opcional añade más fotos para la ficha de detalle.

Lo que la convierte en una imagen «de producción» y no en un simple src es cómo se monta: en AVIF para pesar poco, con width y height fijos para no romper la maqueta, con un alt que la describe, y con una estrategia de carga (eager/lazy + fetchpriority) que prioriza lo que el visitante ve primero. Esas cuatro decisiones son el tema de esta pieza.

Función e importancia

¿Para qué sirve?

Hace que la foto venda sin penalizar: AVIF para pesar poco, dimensiones fijas para no saltar la maqueta (CLS), y alt descriptivo para ser accesible e indexable. Todo medido por Core Web Vitals.

Su función es entrar por los ojos. En un catálogo, la foto es lo primero que mira el visitante y lo que decide si se detiene en un producto. Pero una imagen mal montada es también la causa más común de un sitio lento: pesada, sin dimensiones, cargando todas a la vez. Esta pieza convierte la foto en un activo, no en un lastre.

Y lo hace cuidando lo que Google mide. AVIF baja el peso (carga rápida); width/height fijos eliminan el CLS (la maqueta no salta); el alt la vuelve accesible e indexable; y la carga priorizada cuida el LCP (la foto principal aparece pronto). Son métricas de Core Web Vitals, sí, pero antes que eso son la diferencia entre un catálogo que se siente ágil y uno que frustra.

Ligero por defecto

AVIF reduce el peso de cada foto a una fracción de un JPG sin pérdida visible. En un catálogo de decenas de productos, eso es la diferencia entre una vitrina que pinta al instante y una que arrastra al visitante por segundos de carga —sobre todo en datos móviles—.

Cero saltos de maqueta (CLS)

Con width y height fijos, el navegador reserva el espacio exacto de cada imagen antes de descargarla. El contenido no «salta» cuando la foto entra: el visitante no pierde el botón que iba a tocar. Es una de las métricas de Core Web Vitals que Google mide y premia.

Accesible y rastreable

El alt descriptivo hace la imagen legible para lectores de pantalla y la indexa en la búsqueda de imágenes con su keyword. No es relleno SEO: es la misma frase que ayuda a una persona y a un buscador a entender qué muestra la foto.

Anatomía

¿Qué la compone?

Cuatro decisiones: la ruta obligatoria validada por regex, el formato AVIF ligero, el alt descriptivo con keyword, y el montaje (width/height fijos para CLS + carga eager/lazy/fetchpriority para LCP).

Cada decisión cubre un riesgo. La regex evita la imagen rota; AVIF evita el peso; el alt evita dejar fuera a quien no ve y al buscador; las dimensiones fijas evitan el salto de maqueta; y la carga priorizada evita que la foto importante llegue tarde. Ninguna es decorativa: cada una protege una métrica.

Abajo, el ejemplo en vivo —una imagen montada y anotada—. Cada punto numerado se desglosa en su tarjeta: qué resuelve y qué atributo o regla del sistema lo respalda.

1

Ruta obligatoria y validada

image NO es opcional, y se valida con una regex: debe ser una ruta absoluta bajo /images/. Zod rechaza en build cualquier otra cosa (ruta relativa, URL externa, carpeta equivocada). Resultado: no existe un producto sin foto en el catálogo, ni una ruta rota que se descubra en producción.

Dato image: z.string().regex(/^/images//)

2

AVIF — el formato ligero

Las fotos del sitio son AVIF: una imagen de catálogo pesa una fracción de un JPG o PNG equivalente, con la misma calidad percibida. Menos peso = carga más rápida = mejor Core Web Vitals y menos datos para el visitante en móvil.

Dato /images/productos/*.avif

3

El texto alternativo (alt)

El alt describe lo que se ve e incluye la palabra clave del producto («Casco de seguridad rojo NOM-115»). Sirve a quien no puede ver la imagen (lectores de pantalla) y da contexto al buscador. Si no se pasa, cae al título —funcional pero pobre—; escríbelo a propósito.

Dato imageAlt → <img alt="…">

4

Dimensiones + carga (CLS · LCP)

La imagen lleva width y height fijos (640×360, 16:9) para reservar el hueco antes de cargar: cero CLS, cero saltos de maqueta. La carga es eager para las primeras del grid (la 1ª con fetchpriority="high" → LCP) y lazy para el resto.

Dato width · height · loading · fetchpriority

Variantes

Cómo se usa la imagen

Desde una foto única (lo común) hasta una galería en la ficha, un badge superpuesto o la primera foto con prioridad LCP. Todas son configuraciones reales; la última es la red de seguridad, no un diseño.

No hay un único montaje: hay una foto obligatoria y varias formas de aprovecharla. Un producto simple va con una imagen; uno visual suma galería; uno con norma lleva badge; el primero del grid se prioriza para el LCP. Y por debajo, la garantía: sin imagen válida, no hay producto.

Abajo, cinco configuraciones —cuatro son formas reales de usar la imagen; la quinta es lo que pasa cuando falta—. Cada una con el contexto donde aplica.

  • Foto única (la del catálogo)

    Card + ficha · Default

    El campo image: una sola foto 16:9 que usa la card del catálogo y abre la ficha. Es lo mínimo y lo más común. Con su alt y sus dimensiones fijas, cubre el 90% de los productos.

  • Con galería (ficha)

    Producto visual · Equipos

    gallery[] añade vistas extra. La ficha (L4) pinta una imagen grande + miniaturas debajo; la card del catálogo sigue usando solo image. Para productos que se entienden mejor desde varios ángulos.

  • Con badge sobre la foto

    Norma · Promoción

    El badge (categoría, norma «NOM-115», etiqueta «Nuevo») se superpone arriba a la izquierda de la imagen, sobre un overlay en degradado. Comunica sin robar espacio al título. Es la misma foto, con una capa de contexto.

  • LCP priority (primera del grid)

    Sobre el pliegue · Rendimiento

    La primera card recibe priority: su imagen carga en eager con fetchpriority="high" para ser ella —y no el hero— el LCP que mide Lighthouse. Las 4 primeras quedan eager por índice; el resto, lazy.

  • Sin imagen válida → build falla

    Garantía · No es una variante

    No es un diseño: es la red de seguridad. Si un .md omite image o pone una ruta fuera de /images/, Zod detiene el build. Nunca hay un producto «sin foto» o con una imagen rota en producción.

Responsive y móvil

La foto, en el teléfono

La imagen es fluida (ocupa el ancho de la card sin desbordar ni deformarse) y a la vez anti-CLS (reserva el hueco con width/height). En móvil, donde los datos cuestan, AVIF pesa poco y la primera foto carga con prioridad.

En el teléfono la foto hace dos cosas a la vez que parecen opuestas: es fluida —escala al ancho de la card sin desbordar el viewport— y al mismo tiempo reserva su hueco exacto con las dimensiones del HTML, así que no hay salto cuando carga. Una (max-width:100%) la pone el CSS; la otra (width/height) el atributo. Juntas: imagen flexible sin CLS.

Y donde los datos móviles cuestan, AVIF es el que más rinde: la misma foto, una fracción del peso. La primera del grid carga con prioridad para que el visitante vea algo de inmediato; el resto espera al scroll. Abajo, los dos patrones con su receta.

1 · Fluida y anti-CLS a la vez

max-width: 100% la hace fluida (no desborda); width/height en el HTML reservan el hueco (no salta). El CSS y el atributo colaboran: foto flexible sin CLS.

CSS · imagen fluida + anti-CLS
/* tokens.css + mobile.css · la foto, fluida y sin desbordar. */
img, picture, video, canvas, svg { max-width: 100%; }  /* nunca más ancha que su caja */

@media (max-width: 768px) {
  img, video { height: auto; }   /* escala manteniendo la proporción 16:9 */
}
/* Combinado con width/height en el HTML: fluida AL MISMO TIEMPO que anti-CLS. */

2 · La primera foto, con prioridad (LCP)

La 1ª card del grid sobre el pliegue recibe priority: su foto carga en eager con fetchpriority="high". El resto, lazy: el visitante ve la vitrina al instante y lo de abajo entra con el scroll.

Astro · LCP en la primera card
/* LCP · la PRIMERA foto del grid carga con prioridad.
   El resto, lazy: el visitante ve la vitrina rápido y las fotos de
   abajo entran mientras hace scroll. Una sola priority por página. */

// En el map del grid:
{productos.map((p, i) => (
  <ProductCard
    image={p.data.image}
    index={i}              // las 4 primeras → eager (auto)
    priority={i === 0}     // la 1ª → fetchpriority="high" (LCP)
  />
))}

Posición

¿Dónde viven las fotos?

Los archivos AVIF viven en /public/images/productos/ y se referencian como /images/... La ruta se declara en el frontmatter de cada .md; el montaje (dimensiones, carga) lo hace la ProductCard y la ficha.

Las fotos físicas viven en la carpeta /public/images/productos/ del proyecto; Astro sirve /public en la raíz, así que una foto en /public/images/productos/casco.avif se referencia como /images/productos/casco.avif. Esa es la ruta que va en el frontmatter (image:) y la que la regex valida. Conviene nombrar el archivo con la keyword del producto (casco-seguridad-nom-115.avif), no IMG_4821.avif.

El producto solo declara la ruta; el montaje vive en los componentes: la ProductCard pone width/height, loading y fetchpriority en la card del catálogo, y ProductLayout hace lo propio en la ficha (foto grande + galería). Así la política de imágenes —dimensiones, carga, anti-CLS— se decide una vez para todo el sitio, no foto a foto.

Implementación

Cómo se construye

La regex que valida la ruta (content.config.ts), el comando que optimiza a AVIF antes de subir, y cómo la ProductCard monta el <img> con dimensiones fijas y carga según posición.

La validación es una sola línea: un z.string().regex(/^\/images\//) reutilizable (imagePath) que comparten image y gallery. Antes de subir, la foto se optimiza a AVIF con ImageMagick (q50, ~1280px) y se nombra con la keyword. Esos dos pasos —validar la ruta, optimizar el archivo— son el contrato de entrada de toda imagen del catálogo.

En el render, la ProductCard computa eager = priority || index < 4 y emite un <img> con width/height fijos (16:9), loading eager o lazy según el índice, y fetchpriority="high" solo si priority. Cero JavaScript: HTML estático generado en build. Abajo, las recetas: el esquema, el comando AVIF y el montaje del <img>.

content.config.ts · la imagen obligatoria (regex)
// src/content.config.ts — la imagen, OBLIGATORIA y validada por regex.
const imagePath = z.string().regex(/^\/images\//, {
  message: 'La imagen debe ser ruta absoluta bajo /images/ (ej. /images/productos/foo.avif)',
})

const productos = defineCollection({
  schema: z.object({
    // ...
    image:   imagePath,                    // OBLIGATORIA
    gallery: z.array(imagePath).optional(), // vistas extra para la ficha
  }).strict(),
})
Terminal · optimizar a AVIF antes de subir (ImageMagick)
# Optimizar una foto a AVIF antes de subirla (ImageMagick).
# q50 da excelente calidad percibida a una fracción del peso; 1280px de
# ancho sobra para una card de catálogo. Nombre del archivo = keyword SEO.

magick foto-original.jpg \
  -resize 1280x \
  -quality 50 \
  public/images/productos/casco-seguridad-industrial-nom-115.avif

# Verifica el peso (una foto de catálogo debería rondar 30–80 KB):
ls -lh public/images/productos/*.avif
HTML · cómo monta la imagen la ProductCard
<!-- Cómo monta la imagen la ProductCard: 16:9, dimensiones fijas, carga
     según posición. width/height reservan el hueco (cero CLS); la 1ª del
     grid va eager + fetchpriority alto (LCP), el resto lazy. -->
<img
  src={image}
  alt={imageAlt ?? title}        <!-- alt descriptivo; fallback al título -->
  width="640" height="360"       <!-- 16:9 → reserva el espacio (anti-CLS) -->
  loading={eager ? 'eager' : 'lazy'}
  fetchpriority={priority ? 'high' : undefined}
  decoding="async"
/>

En concreto: imagePath es un z.string().regex(/^\/images\//) que valida image (obligatoria) y cada entrada de gallery (opcional). Una ruta fuera de /images/ detiene el build con un mensaje claro. Las fotos se sirven desde /public/images/; nómbralas con la keyword del producto para el SEO de imágenes.

En la card, la imagen lleva width="640" height="360" (16:9) para reservar el hueco —cero CLS—, loading calculado (eager las 4 primeras, lazy el resto) y fetchpriority="high" solo en la primera con priorityLCP cuidado—. El alt cae a title si no se pasa, pero escribirlo descriptivo es lo que aprovecha la foto para accesibilidad y SEO.

Buenas prácticas

Qué hacer y qué evitar

Una buena foto de catálogo cabe en cinco hábitos: ruta /images válida, AVIF ligero, alt con keyword, dimensiones fijas 16:9 y priority solo en la primera. El resto lo vigila el sistema.

Ninguno de estos hábitos es opcional si quieres un catálogo rápido y accesible. La ruta validada y las dimensiones fijas las refuerza el sistema (la regex y la card); el formato AVIF, el alt descriptivo y el uso correcto de priority quedan a tu disciplina —son justo donde una foto suma o resta—.

La buena noticia: el sistema atrapa lo crítico (una ruta inválida rompe el build) y monta lo repetitivo (dimensiones, carga). Lo que aportas tú es elegir una buena foto, optimizarla y describirla. Abajo, lo que conviene y lo que conviene evitar.

Sí conviene

  • Pon la imagen como ruta absoluta bajo /images/ (ej. /images/productos/casco.avif). Es lo que la regex exige; cualquier otra cosa rompe el build.
  • Optimiza a AVIF antes de subir (q50, ~1280px de ancho). Una foto de catálogo no necesita más resolución y pesa una fracción.
  • Escribe el alt a propósito, describiendo la foto con la keyword del producto. El fallback a title funciona, pero desaprovecha la imagen.
  • Mantén el aspect-ratio 16:9 con width/height fijos (640×360). Si cambias la proporción, cambia ambos números a la vez —si no, vuelve el CLS—.
  • Pasa priority a la PRIMERA card del grid sobre el pliegue (LCP); deja que el resto cargue en lazy automático por índice.

Mejor evita

  • NO uses rutas relativas ni URLs externas en image: la regex /images/ las rechaza en build. La foto vive en /public/images, servida desde la raíz.
  • NO subas JPG/PNG pesados «porque ya los tienes»: una foto de 2 MB hunde el LCP. Conviértela a AVIF; el sistema espera fotos ligeras.
  • NO dejes el alt vacío ni genérico («imagen», «foto»): para el lector de pantalla y el buscador es como no tener alt. Describe con la keyword.
  • NO quites width/height para «que se vea más grande»: pierdes la reserva de espacio y reaparece el CLS que la card cuida por defecto.
  • NO pongas priority en varias cards: más de una fetchpriority="high" compite por el ancho de banda inicial y ninguna gana. Solo la primera.
¿Necesitas ayuda?