↑ top
← Back to blog
EN | ES

R2 como CDN de Imágenes: El Pipeline Completo

De un PNG de 5MB a un WebP de 100KB en R2, servido con los headers correctos. El pipeline completo de imágenes para Cloudflare Workers.

✦ AI Summary

Cloudflare R2 es almacenamiento de objetos — como S3, pero sin tarifas de egress a la red de Cloudflare. Combinado with un Worker that maneja el servicio de imágenes, se convierte en un CDN de imapedia decente.

R2 como CDN de Imágenes: El Pipeline Completo

Cloudflare R2 es almacenamiento de objetos — como S3, pero sin tarifas de egress a la red de Cloudflare. Combinado con un Worker que maneja el servicio de imágenes, se convierte en un CDN de imágenes decente sin pagar por la función de Image Resizing de Cloudflare (que es un add-on de pago).

Aquí está el pipeline completo: cómo las imágenes van de un PNG en mi máquina a un WebP servido desde el edge al tamaño correcto.

El Problema con las Imágenes Crudas

Una foto de cámara o una captura PNG puede fácilmente pesar 3-8MB. Servir eso a cada visitante es caro en dos sentidos: ancho de banda de R2 y tiempo de carga del usuario. Una imagen hero de un post que tarda 3 segundos en cargar en una conexión 4G destruye la experiencia de lectura antes de que empiece.

La solución es comprimir imágenes antes de subirlas. No al momento de servirlas — al momento de publicar.

El Script de Publicación

Cada post puede tener una imagen hero. La coloco en images/<slug>.(png|jpg|webp) y ejecuto npm run publish. El script encuentra la imagen, la comprime, la sube a R2, y establece una bandera en los metadatos del post.

El paso de compresión usa sharp, una librería de procesamiento de imágenes para Node.js:

javascriptconst compressed = await sharp(imageBuffer)
  .resize({ width: 1200, withoutEnlargement: true })
  .webp({ quality: 80 })
  .toBuffer();

Tres cosas suceden:

  1. Redimensionar a máximo 1200px de ancho — la altura escala proporcionalmente, las imágenes en retrato se mantienen en retrato
  2. Convertir a WebP — WebP es aproximadamente 30% más pequeño que JPEG a la misma calidad visual
  3. Calidad 80% — visualmente indistinguible de 100% para fotografías a este tamaño

El resultado: un PNG de 5MB se convierte en aproximadamente 80-150KB WebP. Una reducción de 30-60x.

Subir a R2

Después de la compresión, el script sube a R2 usando la API de Wrangler:

bashwrangler r2 object put dwsa-posts/<slug> \
  --file images/<slug>.webp \
  --content-type image/webp

La clave de R2 coincide exactamente con el slug del post. El Worker lo recupera por slug al servir la ruta de imagen.

Después de la subida, el script establece image: true en el frontmatter del post y actualiza el índice de KV. El Worker lee esta bandera para decidir si usar la imagen hero o la imagen OG por defecto para el meta tag og:image del post.

Sirviendo la Imagen

El Worker maneja /blog/:slug/image:

javascriptconst obj = await env.POSTS_BUCKET.get(slug);
const headers = new Headers();
obj.writeHttpMetadata(headers);
headers.set("Cache-Control", "public, s-maxage=31536000, max-age=86400");
return new Response(obj.body, { headers });

writeHttpMetadata() copia el Content-Type almacenado desde R2 a la respuesta. Los headers de caché le dicen al edge de Cloudflare que cachee esto durante un año — la imagen no cambiará una vez subida, y si lo hace, la nueva versión tiene una nueva URL (slug diferente).

La Conexión con la Imagen OG

Cuando un post tiene una imagen hero, los meta tags OG apuntan a ella:

html<meta property="og:image" content="https://dwsa-mm.com/blog/mi-post/image" />

Twitter, WhatsApp y LinkedIn renderizan esto correctamente porque es una URL WebP propiamente dicha con el header Content-Type correcto. La ruta SVG dinámica del Worker (/blog/:slug/og) existe como herramienta de debug, pero el SVG no funciona como og:image — las plataformas sociales esperan un formato rasterizado.

Estáticos del Sitio: El Mismo Patrón

El mismo pipeline maneja los assets estáticos del sitio — favicon, logo, imagen OG por defecto, video hero. Viven en static/ y CI los sube a R2 cuando los archivos cambian:

static/favicon.png       →  clave R2: "favicon"    →  servido en /favicon.ico
static/logo.png          →  clave R2: "logo"        →  servido en /logo
static/og-default.webp   →  clave R2: "og-default"  →  servido en /og-image

El Worker verifica estas rutas antes que las rutas del blog, así que siempre son rápidas independientemente del estado de KV.

Lo Que No Obtienes Sin Image Resizing

La función de Image Resizing de pago de Cloudflare sirve diferentes tamaños basados en parámetros width en la URL y optimiza automáticamente para el navegador que hace la petición. Sin ella, sirves un tamaño para todos — el WebP de 1200px.

Para un blog, un tamaño está bien. La imagen hero siempre se muestra a ancho completo en escritorio, y en móvil CSS la escala hacia abajo. El WebP de 1200px a 100KB también carga rápido en móvil.

Si necesitaras servir imágenes retina, miniaturas, o crops art-directed por breakpoint, necesitarías Image Resizing o un enfoque diferente. Para un blog personal, un WebP comprimido por post es la decisión correcta.

Was this helpful?