@growth-labs/media
v0.1.4
Published
Astro integration for image and media management on Cloudflare. R2 storage, Cloudflare Image Transformations URL generation, responsive image components, upload pipeline helpers.
Downloads
530
Readme
@growth-labs/media
Astro integration for image and media management on Cloudflare. R2 storage, Cloudflare Image Transformations URL generation, responsive image components, upload pipeline helpers.
Pattern: Store one high-res original in R2, let Image Transformations generate all variants at the edge. Never upload pre-resized copies.
Scope: Images and R2-backed audio (podcast MP3s, narrations) with optional package route authorization. HLS video lives in @growth-labs/video.
Config
import media from '@growth-labs/media'
media({
publicDomain: 'media.fedweek.com', // Custom domain on R2 bucket
r2Binding: 'MEDIA_BUCKET', // Wrangler binding name
defaultQuality: 80,
defaultGravity: 'auto', // 'auto' | 'face' | 'center' | ...
formatAuto: true, // AVIF/WebP/JPEG auto-negotiation
sourceMinWidth: 2400, // Build-time warning threshold
sourceMinHeight: 1350,
uploadRoute: { enabled: false, maxFileSizeMb: 25, requireAuth: true },
audioRoute: {
enabled: false,
path: '/api/media/audio',
// Serialized with Function.prototype.toString(); keep self-contained.
authorize: ({ locals }) => Boolean(locals.user),
},
customVariants: [{ name: 'custom', width: 800, height: 450, fit: 'cover' }],
})What It Injects
Middleware: Cache-Control headers for media responses.
Routes: POST /api/media/upload (if uploadRoute.enabled) and GET /api/media/audio/[...key] (if audioRoute.enabled).
The audio route resolves the configured R2 binding through Cloudflare Workers env bindings, serves files under prefixes.audio, supports an optional self-contained authorization hook, and forwards browser Range headers to R2 using Cloudflare's supported { range: request.headers } option.
Vite defines: __GROWTH_LABS_MEDIA_DOMAIN__, __GROWTH_LABS_MEDIA_QUALITY__, __GROWTH_LABS_MEDIA_GRAVITY__
Components
---
import { Image, Figure, Gallery, AudioPlayer } from '@growth-labs/media/components'
---
<Image src="articles/my-article.jpg" alt="Description" variant="hero" />
<Figure src="articles/photo.jpg" alt="..." caption="Photo credit" />
<AudioPlayer src="audio/episode-1.mp3" title="Episode 1" narrator="Simon Whistler" />Standalone Utilities
import { imageUrl, imageSrcset, audioUrl } from '@growth-labs/media/utils'
import { uploadToR2, mediaKey, ogKey } from '@growth-labs/media/utils'
import { validateImage, warmTransformationCache } from '@growth-labs/media/utils'
imageUrl('media.fedweek.com', 'articles/photo.jpg', { width: 800, quality: 80 })
// → https://media.fedweek.com/cdn-cgi/image/width=800,quality=80,format=auto/articles/photo.jpg
imageSrcset('media.fedweek.com', 'articles/photo.jpg')
// → srcset string with 400w, 800w, 1200w, 1600w
mediaKey('articles', 'my-slug', 'jpg') // → 'articles/my-slug.jpg'
ogKey('/news/my-article') // → 'og/news/my-article.png'Built-in Variants
| Name | Width | Height | Aspect | Use | |------|-------|--------|--------|-----| | thumbnail | 480 | 270 | 16:9 | Thumbnails | | card | 600 | 338 | 16:9 | Card images | | article | 900 | 506 | 16:9 | Article body images | | hero | 1200 | 675 | 16:9 | Hero / Discover 16:9 | | full | 1600 | 900 | 16:9 | Full-width | | discover-16x9 | 1200 | 675 | 16:9 | Google Discover | | discover-4x3 | 1200 | 900 | 4:3 | Google Discover | | discover-1x1 | 1200 | 1200 | 1:1 | Google Discover |
R2 Key Migration
Persist R2 object keys like articles/my-story.jpg, not legacy external image IDs or fully transformed URLs. For legacy records, strip the media domain and any /cdn-cgi/image/.../ prefix, store the remaining object key, then call imageUrl(), imageSrcset(), or getDiscoverImages() at render time.
R2 Bucket Structure
{site}-public-media/
articles/{slug}.jpg # Article hero images
authors/{name}.jpg # Author headshots
logos/header.png # Branding
store/{product-id}.jpg # Store product images
videos/{id}/poster.jpg # Video thumbnails
audio/{slug}.mp3 # Narrations, podcasts
og/{page-path}.png # OG images (from @growth-labs/opengraph)
health-check.txt # Uptime canaryWrangler Bindings
[[r2_buckets]]
binding = "MEDIA_BUCKET"
bucket_name = "fedweek-public-media"Image Transformations must be enabled on the R2 bucket's custom domain (Cloudflare dashboard → Images → Transformations).
Key Rules
- Never use Cloudinary, Imgix, Cloudflare Images storage, or Cloudflare Stream for this package
- Min source: 2400×1350 (2x headroom for largest variant)
- No text overlays on hero images (Google Discover penalty)
format=autoalways — let Cloudflare negotiate AVIF/WebP/JPEG- Cache is permanent per unique transformation — billed once/month
ogKey()aligns with@growth-labs/opengraph's R2 key convention
