Deliver fast, responsive images with zero layout shift. This guide covers the SDK's image utilities, pre-computed variants, responsive <img> and <picture> patterns, custom Next.js loaders, blur placeholders, OG images, and focal-point cropping.
Prerequisites
You should have the Lynkow SDK installed and a client initialized. If not, see Guide 1: Quick Start.
// lib/lynkow.ts
import { createClient } from 'lynkow'
export const lynkow = createClient({
siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID!,
fetchOptions: {
next: { revalidate: 60 },
},
})1. How Lynkow serves images
All media uploaded to Lynkow is stored on Cloudflare R2 and served through Cloudflare's CDN. When you request a content item, the API returns:
featuredImage-- the original, untransformed URLfeaturedImageVariants-- an object of pre-computed variant URLs, each optimized for a specific use case
The CDN automatically negotiates the best format (WebP or AVIF) based on the browser's Accept header, so you never need to manage format conversion yourself.
The SDK provides two additional methods on lynkow.media for on-the-fly transformations:
Method | Purpose |
|---|---|
| Build a complete |
| Build a single transformed URL with specific dimensions |
Both methods are pure utilities -- they construct Cloudflare /cdn-cgi/image/ URLs without making any API calls. They work on server and browser, and handle null/undefined safely (returning an empty string).
2. Pre-computed ImageVariants
Every content item with a featuredImage returns a featuredImageVariants object. These are ready-to-use URLs optimized for common UI patterns:
interface ImageVariants {
thumbnail?: string // 400x300, fit: cover -- grid thumbnails, admin previews
card?: string // 600x400, fit: cover -- article cards, listing pages
content?: string // 1200w, fit: scale-down -- inline content images
medium?: string // 960w, fit: scale-down -- medium-width containers
hero?: string // 1920w, fit: scale-down -- full-width hero banners
og?: string // 1200x630, fit: cover -- Open Graph / social sharing
avatar?: string // 128x128, fit: cover -- user avatars, author photos
full?: string // 2560w, fit: scale-down -- lightbox, full-resolution
}When to use variants vs. srcset/transform:
Use case | Approach |
|---|---|
Article card at a fixed size |
|
Hero banner, single size |
|
OG meta tag |
|
Responsive |
|
Custom size not in the presets |
|
Next.js | Custom loader with |
Pre-computed variants are ideal when you need a single, known size. Use srcset when you need the browser to pick the best width at runtime.
3. Hero banners with featuredImageVariants.hero
The hero variant is 1920 pixels wide with scale-down fit, suitable for full-width banners:
// app/blog/[slug]/page.tsx
import { lynkow } from '@/lib/lynkow'
interface Props {
params: Promise<{ slug: string }>
}
export default async function ArticlePage({ params }: Props) {
const { slug } = await params
const article = await lynkow.contents.getBySlug(slug)
return (
<article>
{article.featuredImage && (
<div className="relative w-full h-[60vh] overflow-hidden">
<img
src={article.featuredImageVariants?.hero || article.featuredImage}
alt={article.title}
className="w-full h-full object-cover"
loading="eager"
decoding="async"
fetchPriority="high"
/>
</div>
)}
<div className="max-w-3xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold">{article.title}</h1>
{/* ... article body */}
</div>
</article>
)
}Use loading="eager" and fetchPriority="high" for above-the-fold hero images. The browser should not lazy-load them.
4. Article cards with featuredImageVariants.card
The card variant is 600x400 with cover fit, cropped to a consistent aspect ratio:
// components/article-card.tsx
import Link from 'next/link'
interface ArticleCardProps {
title: string
slug: string
excerpt: string | null
featuredImage: string | null
featuredImageVariants: Record<string, string> | null
publishedAt: string
}
export function ArticleCard({
title,
slug,
excerpt,
featuredImage,
featuredImageVariants,
publishedAt,
}: ArticleCardProps) {
const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
return (
<article className="group border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
{featuredImage && (
<Link href={`/blog/${slug}`}>
<img
src={featuredImageVariants?.card || featuredImage}
alt={title}
width={600}
height={400}
className="w-full aspect-[3/2] object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy"
decoding="async"
/>
</Link>
)}
<div className="p-6">
<time dateTime={publishedAt} className="text-sm text-gray-500">
{formattedDate}
</time>
<h2 className="mt-2 text-xl font-semibold">
<Link href={`/blog/${slug}`} className="hover:text-blue-600 transition-colors">
{title}
</Link>
</h2>
{excerpt && <p className="mt-2 text-gray-600 line-clamp-3">{excerpt}</p>}
</div>
</article>
)
}Setting explicit width and height attributes prevents layout shift while the image loads.
5. Building responsive <img> with srcset and sizes
For images that need to adapt to the viewport, use lynkow.media.srcset() to generate a standards-compliant srcset attribute:
// components/responsive-image.tsx
import { lynkow } from '@/lib/lynkow'
interface ResponsiveImageProps {
src: string
alt: string
variants?: Record<string, string> | null
sizes?: string
priority?: boolean
className?: string
}
export function ResponsiveImage({
src,
alt,
variants,
sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
priority = false,
className,
}: ResponsiveImageProps) {
const srcSet = lynkow.media.srcset(src, {
widths: [400, 800, 1200, 1920],
quality: 80,
})
// Use the card variant as a sensible default src for browsers
// that do not support srcset
const defaultSrc = variants?.card || src
return (
<img
src={defaultSrc}
srcSet={srcSet || undefined}
sizes={sizes}
alt={alt}
className={className}
loading={priority ? 'eager' : 'lazy'}
decoding="async"
/>
)
}How srcset + sizes work together
The srcset attribute tells the browser which image widths are available:
https://cdn.../cdn-cgi/image/w=400,.../image.jpg 400w,
https://cdn.../cdn-cgi/image/w=800,.../image.jpg 800w,
https://cdn.../cdn-cgi/image/w=1200,.../image.jpg 1200w,
https://cdn.../cdn-cgi/image/w=1920,.../image.jpg 1920wThe sizes attribute tells the browser how wide the image will be rendered at each viewport width. The browser then picks the smallest image that covers the rendered width (accounting for device pixel ratio).
Common sizes patterns:
Layout |
|
|---|---|
Full-width hero |
|
Two-column grid |
|
Three-column grid |
|
Sidebar image |
|
Fixed-width card |
|
Customizing widths and quality
Pass custom options to srcset():
// Fewer breakpoints for a sidebar image
const srcSet = lynkow.media.srcset(imageUrl, {
widths: [300, 600],
quality: 75,
})
// More breakpoints for a hero image with crop
const heroSrcSet = lynkow.media.srcset(imageUrl, {
widths: [640, 960, 1280, 1920, 2560],
fit: 'cover',
quality: 85,
gravity: '0.5x0.3', // focal point: center-horizontal, upper-third
})6. Building <picture> for art direction
When you need different crops at different breakpoints (e.g., a landscape crop on desktop and a square crop on mobile), use the <picture> element with lynkow.media.transform():
// components/hero-picture.tsx
import { lynkow } from '@/lib/lynkow'
interface HeroPictureProps {
src: string
alt: string
className?: string
}
export function HeroPicture({ src, alt, className }: HeroPictureProps) {
// Desktop: wide landscape crop
const desktopSrc = lynkow.media.transform(src, {
w: 1920,
h: 640,
fit: 'cover',
quality: 85,
})
// Tablet: 4:3 crop
const tabletSrc = lynkow.media.transform(src, {
w: 1024,
h: 768,
fit: 'cover',
quality: 85,
})
// Mobile: square crop
const mobileSrc = lynkow.media.transform(src, {
w: 640,
h: 640,
fit: 'cover',
quality: 80,
})
return (
<picture>
<source media="(min-width: 1024px)" srcSet={desktopSrc} />
<source media="(min-width: 640px)" srcSet={tabletSrc} />
<img
src={mobileSrc}
alt={alt}
className={className}
loading="eager"
decoding="async"
fetchPriority="high"
/>
</picture>
)
}You can combine <picture> with srcset for art direction plus resolution switching:
export function HeroPictureResponsive({ src, alt }: HeroPictureProps) {
// Desktop: wide landscape crop at multiple resolutions
const desktopSrcSet = lynkow.media.srcset(src, {
widths: [1280, 1920, 2560],
fit: 'cover',
quality: 85,
})
// Mobile: square crop at multiple resolutions
const mobileSrcSet = lynkow.media.srcset(src, {
widths: [400, 640, 800],
fit: 'cover',
quality: 80,
})
const fallbackSrc = lynkow.media.transform(src, { w: 800, fit: 'cover' })
return (
<picture>
<source
media="(min-width: 1024px)"
srcSet={desktopSrcSet}
sizes="100vw"
/>
<source
media="(max-width: 1023px)"
srcSet={mobileSrcSet}
sizes="100vw"
/>
<img
src={fallbackSrc}
alt={alt}
loading="eager"
decoding="async"
fetchPriority="high"
/>
</picture>
)
}7. Custom Next.js Image loader with transform()
If you prefer the Next.js <Image> component for its built-in lazy loading, priority hints, and placeholder support, create a custom loader that delegates to Lynkow's CDN:
// lib/lynkow-image-loader.ts
import { lynkow } from './lynkow'
interface ImageLoaderParams {
src: string
width: number
quality?: number
}
export function lynkowImageLoader({ src, width, quality }: ImageLoaderParams): string {
return lynkow.media.transform(src, {
w: width,
quality: quality || 80,
format: 'auto',
fit: 'scale-down',
})
}Use it with the <Image> component:
// components/optimized-image.tsx
import Image from 'next/image'
import { lynkowImageLoader } from '@/lib/lynkow-image-loader'
interface OptimizedImageProps {
src: string
alt: string
width: number
height: number
priority?: boolean
className?: string
sizes?: string
}
export function OptimizedImage({
src,
alt,
width,
height,
priority = false,
className,
sizes,
}: OptimizedImageProps) {
return (
<Image
loader={lynkowImageLoader}
src={src}
alt={alt}
width={width}
height={height}
priority={priority}
className={className}
sizes={sizes}
/>
)
}Usage in a page:
import { lynkow } from '@/lib/lynkow'
import { OptimizedImage } from '@/components/optimized-image'
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const article = await lynkow.contents.getBySlug(slug)
return (
<article>
{article.featuredImage && (
<OptimizedImage
src={article.featuredImage}
alt={article.title}
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
/>
)}
<h1>{article.title}</h1>
</article>
)
}Important: When using the Next.js <Image> component with a custom loader, you must also configure remotePatterns in next.config.ts to allow the Lynkow CDN domain:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '*.lynkow.com',
},
],
},
}
export default nextConfig8. Blur placeholder with tiny transform
Generate a tiny, low-quality image as a blur placeholder to show while the full image loads. This eliminates layout shift and gives users an immediate visual preview.
// components/blur-image.tsx
import { lynkow } from '@/lib/lynkow'
interface BlurImageProps {
src: string
alt: string
variants?: Record<string, string> | null
sizes?: string
priority?: boolean
className?: string
}
export function BlurImage({
src,
alt,
variants,
sizes = '100vw',
priority = false,
className,
}: BlurImageProps) {
// Generate a 20px-wide placeholder -- tiny enough to inline
const blurSrc = lynkow.media.transform(src, {
w: 20,
quality: 50,
format: 'jpeg',
})
const srcSet = lynkow.media.srcset(src, {
widths: [400, 800, 1200, 1920],
})
const defaultSrc = variants?.hero || variants?.content || src
return (
<div className={`relative overflow-hidden ${className || ''}`}>
{/* Blur placeholder (shows immediately, tiny payload) */}
<img
src={blurSrc}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover blur-xl scale-110"
/>
{/* Full-resolution image (loads on top) */}
<img
src={defaultSrc}
srcSet={srcSet || undefined}
sizes={sizes}
alt={alt}
loading={priority ? 'eager' : 'lazy'}
decoding="async"
className="relative w-full h-full object-cover"
onLoad={(e) => {
// Hide the blur placeholder once the real image loads
const prev = e.currentTarget.previousElementSibling as HTMLElement
if (prev) prev.style.opacity = '0'
}}
/>
</div>
)
}The 20px-wide JPEG is typically under 500 bytes and loads almost instantly. The blur-xl scale-110 CSS creates a smooth blurred background while the full image downloads.
For server-side blur with the Next.js <Image> component, you can fetch the tiny image and convert it to a base64 data URL:
// lib/blur-data-url.ts
import { lynkow } from './lynkow'
export async function getBlurDataUrl(imageUrl: string): Promise<string> {
const tinyUrl = lynkow.media.transform(imageUrl, {
w: 20,
quality: 50,
format: 'jpeg',
})
const response = await fetch(tinyUrl)
const buffer = await response.arrayBuffer()
const base64 = Buffer.from(buffer).toString('base64')
return `data:image/jpeg;base64,${base64}`
}Then use it with <Image>:
import Image from 'next/image'
import { lynkowImageLoader } from '@/lib/lynkow-image-loader'
import { getBlurDataUrl } from '@/lib/blur-data-url'
interface Props {
src: string
alt: string
width: number
height: number
}
export async function BlurNextImage({ src, alt, width, height }: Props) {
const blurDataURL = await getBlurDataUrl(src)
return (
<Image
loader={lynkowImageLoader}
src={src}
alt={alt}
width={width}
height={height}
placeholder="blur"
blurDataURL={blurDataURL}
/>
)
}9. OG images for social sharing
Lynkow provides two sets of OG image variants:
featuredImageVariants.og-- derived from the content's featured image (1200x630,coverfit)ogImageVariants.og-- derived from a dedicated OG image field, if one was uploaded separately
Use them in generateMetadata():
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
import { lynkow } from '@/lib/lynkow'
const BASE_URL = 'https://example.com'
interface Props {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const content = await lynkow.contents.getBySlug(slug)
// Prefer the dedicated OG image, fall back to the featured image variant
const ogImageUrl =
content.ogImageVariants?.og ||
content.featuredImageVariants?.og ||
null
return {
title: content.metaTitle || content.title,
description: content.metaDescription || undefined,
openGraph: {
title: content.metaTitle || content.title,
description: content.metaDescription || undefined,
url: `${BASE_URL}/blog/${slug}`,
type: 'article',
images: ogImageUrl
? [{ url: ogImageUrl, width: 1200, height: 630, alt: content.title }]
: [],
},
twitter: {
card: 'summary_large_image',
title: content.metaTitle || content.title,
description: content.metaDescription || undefined,
images: ogImageUrl ? [ogImageUrl] : [],
},
}
}The OG variant is always 1200x630 pixels with cover fit, which is the standard aspect ratio for Facebook, LinkedIn, and Twitter (X) cards.
10. Gravity and focal points
When cropping images with fit: 'cover' or fit: 'crop', the gravity option controls which part of the image is preserved. This is essential for portrait photos, product shots, or any image where the subject is not centered.
Automatic face detection
// Use face detection for author avatars
const avatarUrl = lynkow.media.transform(authorPhoto, {
w: 128,
h: 128,
fit: 'cover',
gravity: 'face',
})Coordinate-based focal points
Focal points use the format {x}x{y} where both values are between 0 and 1. The origin (0,0) is the top-left corner.
// Focus on the upper-right area of the image
const croppedUrl = lynkow.media.transform(imageUrl, {
w: 800,
h: 400,
fit: 'cover',
gravity: '0.8x0.2',
})Common focal point values:
Position | Gravity value |
|---|---|
Center (default) |
|
Top center |
|
Bottom center |
|
Left center |
|
Right center |
|
Face detection |
|
Portrait vs. landscape crops
// components/adaptive-thumbnail.tsx
import { lynkow } from '@/lib/lynkow'
interface AdaptiveThumbnailProps {
src: string
alt: string
focalPoint?: { x: number; y: number }
}
export function AdaptiveThumbnail({ src, alt, focalPoint }: AdaptiveThumbnailProps) {
const gravity = focalPoint ? `${focalPoint.x}x${focalPoint.y}` : undefined
// Landscape crop for desktop
const landscapeSrc = lynkow.media.transform(src, {
w: 800,
h: 450,
fit: 'cover',
gravity,
})
// Square crop for mobile
const squareSrc = lynkow.media.transform(src, {
w: 400,
h: 400,
fit: 'cover',
gravity,
})
return (
<picture>
<source media="(min-width: 640px)" srcSet={landscapeSrc} />
<img
src={squareSrc}
alt={alt}
className="w-full object-cover"
loading="lazy"
decoding="async"
/>
</picture>
)
}With srcset for responsive focal-point crops:
const srcSet = lynkow.media.srcset(portraitPhoto, {
widths: [400, 800, 1200],
fit: 'cover',
gravity: '0.5x0.3', // keep the upper portion (face area)
quality: 85,
})11. Complete ResponsiveImage component
This production-ready component combines srcset, variants, blur placeholder, focal points, and priority loading:
// components/responsive-image.tsx
import { lynkow } from '@/lib/lynkow'
interface ResponsiveImageProps {
/** Original image URL from the Lynkow API */
src: string | null | undefined
/** Alt text for accessibility */
alt: string
/** Pre-computed image variants from the API */
variants?: Record<string, string> | null
/** CSS sizes attribute for responsive layout */
sizes?: string
/** Load eagerly for above-the-fold images */
priority?: boolean
/** Additional CSS classes */
className?: string
/** Custom srcset widths (default: [400, 800, 1200, 1920]) */
widths?: number[]
/** Focal point for crop-based transforms */
gravity?: string
/** Image quality 1-100 (default: 80) */
quality?: number
/** Resize fit mode */
fit?: 'cover' | 'contain' | 'scale-down' | 'crop'
/** Explicit width for aspect ratio */
width?: number
/** Explicit height for aspect ratio */
height?: number
}
export function ResponsiveImage({
src,
alt,
variants,
sizes = '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
priority = false,
className,
widths = [400, 800, 1200, 1920],
gravity,
quality = 80,
fit = 'scale-down',
width,
height,
}: ResponsiveImageProps) {
if (!src) return null
const srcSet = lynkow.media.srcset(src, { widths, gravity, quality, fit })
const defaultSrc = variants?.card || variants?.content || src
// Generate a tiny blur placeholder URL
const blurSrc = lynkow.media.transform(src, {
w: 20,
quality: 50,
format: 'jpeg',
})
return (
<div className={`relative overflow-hidden ${className || ''}`}>
{/* Blur placeholder */}
{blurSrc && (
<img
src={blurSrc}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover blur-xl scale-110 transition-opacity duration-500"
/>
)}
{/* Full image */}
<img
src={defaultSrc}
srcSet={srcSet || undefined}
sizes={sizes}
alt={alt}
width={width}
height={height}
loading={priority ? 'eager' : 'lazy'}
decoding="async"
fetchPriority={priority ? 'high' : undefined}
className="relative w-full h-full object-cover"
onLoad={(e) => {
const blur = e.currentTarget.previousElementSibling as HTMLElement | null
if (blur) blur.style.opacity = '0'
}}
/>
</div>
)
}12. Complete ArticleCard component with optimized image
Putting it all together -- a card component that uses the card variant as a direct src, with srcset for retina displays, and a blur placeholder:
// components/article-card.tsx
import Link from 'next/link'
import { lynkow } from '@/lib/lynkow'
interface ArticleCardProps {
title: string
slug: string
excerpt: string | null
featuredImage: string | null
featuredImageVariants: Record<string, string> | null
publishedAt: string
category?: { name: string; slug: string } | null
}
export function ArticleCard({
title,
slug,
excerpt,
featuredImage,
featuredImageVariants,
publishedAt,
category,
}: ArticleCardProps) {
const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
// Generate srcset for retina-quality cards (1x at 600, 2x at 1200)
const srcSet = featuredImage
? lynkow.media.srcset(featuredImage, {
widths: [400, 600, 800, 1200],
fit: 'cover',
quality: 80,
})
: ''
// Tiny blur placeholder
const blurSrc = featuredImage
? lynkow.media.transform(featuredImage, {
w: 20,
quality: 40,
format: 'jpeg',
})
: ''
return (
<article className="group border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
{featuredImage && (
<Link href={`/blog/${slug}`} className="block relative overflow-hidden">
{/* Blur placeholder */}
{blurSrc && (
<img
src={blurSrc}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover blur-xl scale-110 transition-opacity duration-500"
/>
)}
{/* Full image */}
<img
src={featuredImageVariants?.card || featuredImage}
srcSet={srcSet || undefined}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
alt={title}
width={600}
height={400}
className="relative w-full aspect-[3/2] object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy"
decoding="async"
onLoad={(e) => {
const blur = e.currentTarget.previousElementSibling as HTMLElement | null
if (blur) blur.style.opacity = '0'
}}
/>
</Link>
)}
<div className="p-6">
<div className="flex items-center gap-3 text-sm text-gray-500">
<time dateTime={publishedAt}>{formattedDate}</time>
{category && (
<>
<span aria-hidden="true">-</span>
<Link
href={`/blog/category/${category.slug}`}
className="text-blue-600 hover:underline"
>
{category.name}
</Link>
</>
)}
</div>
<h2 className="mt-2 text-xl font-semibold">
<Link
href={`/blog/${slug}`}
className="hover:text-blue-600 transition-colors"
>
{title}
</Link>
</h2>
{excerpt && (
<p className="mt-2 text-gray-600 line-clamp-3">{excerpt}</p>
)}
</div>
</article>
)
}Usage on a listing page:
// app/blog/page.tsx
import { lynkow } from '@/lib/lynkow'
import { ArticleCard } from '@/components/article-card'
export default async function BlogPage() {
const { data: articles } = await lynkow.contents.list({
limit: 12,
sort: 'published_at',
order: 'desc',
})
return (
<main className="max-w-7xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold mb-8">Blog</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{articles.map((article) => (
<ArticleCard
key={article.id}
title={article.title}
slug={article.slug}
excerpt={article.excerpt}
featuredImage={article.featuredImage}
featuredImageVariants={article.featuredImageVariants}
publishedAt={article.publishedAt}
category={article.category}
/>
))}
</div>
</main>
)
}API reference summary
lynkow.media.srcset(imageUrl, options?)
Generates a complete srcset attribute string from a Lynkow image URL.
lynkow.media.srcset(
imageUrl: string | null | undefined,
options?: {
widths?: number[] // default: [400, 800, 1200, 1920]
fit?: 'cover' | 'contain' | 'scale-down' | 'crop' // default: 'scale-down'
quality?: number // 1-100, default: 80
gravity?: string // e.g. '0.5x0.3', 'face'
}
): string // Returns srcset string or empty stringlynkow.media.transform(imageUrl, options?)
Generates a single transformed image URL.
lynkow.media.transform(
imageUrl: string | null | undefined,
options?: {
w?: number // target width
h?: number // target height
fit?: 'cover' | 'contain' | 'scale-down' | 'crop' // default: 'scale-down'
quality?: number // 1-100, default: 80
format?: 'auto' | 'webp' | 'avif' | 'jpeg' // default: 'auto'
gravity?: string // e.g. '0.5x0.3', 'face'
dpr?: number // device pixel ratio, 1-4
}
): string // Returns transformed URL or empty stringPre-computed variant presets
Variant | Dimensions | Fit | Use case |
|---|---|---|---|
| 400x300 | cover | Grid thumbnails, admin previews |
| 600x400 | cover | Article cards, listing pages |
| 1200w | scale-down | Inline content images |
| 960w | scale-down | Medium-width containers |
| 1920w | scale-down | Full-width hero banners |
| 1200x630 | cover | Open Graph / social sharing |
| 128x128 | cover | User avatars (gravity: face) |
| 2560w | scale-down | Lightbox, full resolution |