Saltar al contenido

Imágenes remotas y en colecciones

Cómo trata Astro una imagen que no vive en src: por qué una URL remota obliga a declarar width y height, por qué su optimización está desactivada hasta que autorizas el origen con image.domains o image.remotePatterns, qué riesgo cierra esa autorización, y cómo el ayudante image del esquema de una content collection convierte una ruta del frontmatter en un activo local validado y optimizable en el build.

La URL que Astro no conoce

Cuando el src de <Image /> es una cadena con una URL remota, el compilador no tiene el fichero delante: no puede leer su ancho ni su alto sin descargarlo, y no va a descargar medio internet en cada build para averiguarlo. Por eso te traslada a ti esa responsabilidad: en una imagen remota, width y height son obligatorios. No es burocracia, es la misma regla de siempre —ninguna imagen sin dimensiones— aplicada al caso en que Astro no puede deducirlas solo.

ASTRO
---import { Image } from 'astro:assets';---<Image  src="https://images.unsplash.com/photo-123"  width={1200}  height={800}  alt="Muelle al atardecer"/>

Hay una segunda diferencia, más silenciosa. Por defecto, Astro no optimiza una imagen remota: la deja pasar hacia el navegador tal como está, sin reencodear ni redimensionar, limitándose a poner las dimensiones que declaraste. La razón no es técnica sino de confianza, y la desactivamos a propósito en el siguiente paso.

Existe un atajo para no teclear las dimensiones a mano: la prop inferSize. Con ella, Astro descarga la imagen remota durante el build, lee su ancho y su alto reales y los rellena por ti. Es cómodo, pero no gratis: cada inferSize es una petición de red en tiempo de construcción, así que en un sitio con muchas remotas alarga el build y lo hace depender de que esos orígenes respondan a tiempo. Úsalo cuando no conozcas las dimensiones de antemano, y prefiere declararlas tú cuando sí las sepas.

ASTRO
<Image  src="https://images.unsplash.com/photo-123"  inferSize  alt="Muelle al atardecer"/>

Autorizar orígenes: domains y remotePatterns

Para que Astro sí transforme una imagen remota tienes que declarar que su origen es de fiar. Hay dos llaves en la configuración. image.domains acepta una lista de nombres de host exactos; cualquier imagen servida desde uno de ellos entra en la maquinaria de optimización. image.remotePatterns es más expresivo: describe patrones por protocolo, host y ruta, y admite comodines para cubrir subdominios enteros.

TypeScript
// astro.config.mjsimport { defineConfig } from 'astro/config';export default defineConfig({  image: {    domains: ['images.unsplash.com'],    remotePatterns: [{ protocol: 'https', hostname: '**.mi-cdn.com' }],  },});

Con esa declaración, una imagen de images.unsplash.com o de cualquier subdominio de mi-cdn.com pasa a optimizarse como si fuera local: formato moderno, tamaños a medida, todo el arsenal. Las de cualquier otro origen siguen sirviéndose intactas. La diferencia entre una imagen remota optimizada y una que solo cruza el sitio es, literalmente, una línea en la configuración.

Por qué la autorización es explícita

Que la optimización remota venga apagada no es pereza de Astro, sino una decisión de seguridad. El servicio de imágenes es, en el fondo, un motor que recibe una URL, descarga lo que haya allí y lo procesa. Si aceptara cualquier URL, se convertiría en un proxy abierto: alguien podría pedirle que fuera a buscar y transformar imágenes de orígenes arbitrarios usando tu servidor como intermediario, consumiendo tu cómputo y tu ancho de banda, o alcanzando destinos que tu servidor no debería tocar. Obligarte a enumerar los orígenes de confianza cierra esa puerta: el motor solo saldrá a buscar donde tú, explícitamente, le has dado permiso.

El ayudante image en el esquema de una colección

Entre la imagen local con import y la remota con URL hay un tercer caso muy común: imágenes que acompañan al contenido de una content collection y se nombran en su frontmatter. Escribir ahí una ruta como cadena de texto no garantiza nada —puede apuntar a un fichero borrado—, así que el esquema ofrece una forma mejor. Escrito en su variante de función, recibe un ayudante image que valida la ruta contra un fichero real y devuelve sus metadatos, exactamente el objeto que <Image /> sabe optimizar.

TypeScript
import { defineCollection, z } from 'astro:content';import { glob } from 'astro/loaders';const blog = defineCollection({  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),  schema: ({ image }) =>    z.object({      title: z.string(),      cover: image(),      coverAlt: z.string(),    }),});

En el frontmatter de cada entrada, cover es una ruta relativa al propio fichero de la entrada, y image() comprueba en el build que ese activo existe. Si no está, el sitio no compila, igual que faltaría un campo obligatorio. Lo que llega a la plantilla ya no es una cadena frágil, sino el mismo ImageMetadata con ancho, alto y formato que optimizarías desde un import.

ASTRO
---import { Image } from 'astro:assets';import { getEntry } from 'astro:content';const post = await getEntry('blog', 'mi-post');---<Image src={post.data.cover} alt={post.data.coverAlt} />

La elección entre image() y una cadena URL es la elección entre un activo gestionado y una promesa externa. Si la portada vive contigo, image() la valida y la optimiza; si es una URL remota que quieres guardar en el frontmatter, usa z.string().url(), asumiendo que renuncias a la validación del fichero y a la optimización salvo que autorices su origen. Emparejar image() con un coverAlt obligatorio, como ya viste con las collections, convierte además el texto alternativo en parte del contrato.