Documentation Index
Fetch the complete documentation index at: https://mintlify.com/cloudflare/vinext/llms.txt
Use this file to discover all available pages before exploring further.
Image Optimization
vinext uses Cloudflare Images for on-the-fly image resizing and format transcoding in production. Images are optimized at the edge with zero build-time processing.
How It Works
When using next/image, vinext:
- Generates an optimization URL:
/_vinext/image?url=/photo.jpg&w=800&q=75
- Fetches the source image from the ASSETS binding
- Transforms via Cloudflare Images (resize, transcode, compress)
- Returns optimized image with
Cache-Control: public, max-age=31536000, immutable
No build-time optimization or static resizing occurs.
Setup
Automatic Setup (vinext deploy)
vinext deploy configures everything automatically:
This generates:
- ASSETS binding in
wrangler.jsonc
- IMAGES binding in
wrangler.jsonc
- Worker entry with image optimization handler
Manual Setup
{
"name": "my-app",
"main": "worker/index.ts",
"assets": {
"directory": "dist/client",
"binding": "ASSETS",
"not_found_handling": "none"
},
"images": {
"binding": "IMAGES"
}
}
2. Create Worker Entry
import { handleImageOptimization } from 'vinext/server/image-optimization'
import handler from 'vinext/server/app-router-entry'
interface Env {
ASSETS: Fetcher
IMAGES: {
input(stream: ReadableStream): {
transform(options: Record<string, unknown>): {
output(options: { format: string; quality: number }): Promise<{ response(): Response }>
}
}
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === '/_vinext/image') {
return handleImageOptimization(request, {
fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))),
transformImage: async (body, { width, format, quality }) => {
const result = await env.IMAGES.input(body)
.transform(width > 0 ? { width } : {})
.output({ format, quality })
return result.response()
}
})
}
return handler.fetch(request)
}
}
API Reference
handleImageOptimization
handleImageOptimization(
request: Request,
handlers: ImageHandlers
): Promise<Response>
Handles image optimization requests.
Parameters
request (required)
Type: Request
Incoming image optimization request.
handlers (required)
Type: ImageHandlers
Callbacks for fetching and transforming images.
interface ImageHandlers {
fetchAsset: (path: string, request: Request) => Promise<Response>
transformImage?: (
body: ReadableStream,
options: { width: number; format: string; quality: number }
) => Promise<Response>
}
handlers.fetchAsset
Fetch the source image. Typically delegates to env.ASSETS.fetch().
handlers.transformImage (optional)
Transform the image. Omit for passthrough (serves original).
Returns
Promise<Response> - Optimized image response with cache headers.
parseImageParams
parseImageParams(url: URL): { imageUrl: string; width: number; quality: number } | null
Parses and validates image optimization query parameters.
Query Parameters
url (required)
Type: string
Source image path. Must be path-relative (starts with /, not //).
w (required)
Type: number
Target width in pixels. 0 = no resize.
q (optional)
Type: number
Default: 75
Quality (1-100).
Example
const url = new URL('https://example.com/_vinext/image?url=/photo.jpg&w=800&q=75')
const params = parseImageParams(url)
// { imageUrl: '/photo.jpg', width: 800, quality: 75 }
negotiateImageFormat(acceptHeader: string | null): string
Negotiates the best output format based on the Accept header.
Returns
'image/avif' if Accept includes image/avif
'image/webp' if Accept includes image/webp
'image/jpeg' otherwise
Example
const format = negotiateImageFormat('image/avif,image/webp,*/*')
// 'image/avif'
Constants
IMAGE_OPTIMIZATION_PATH
const IMAGE_OPTIMIZATION_PATH = '/_vinext/image'
The pathname that triggers image optimization.
IMAGE_CACHE_CONTROL
const IMAGE_CACHE_CONTROL = 'public, max-age=31536000, immutable'
Standard Cache-Control header for optimized images. Optimized images are immutable because the URL encodes the transform params.
Usage in Components
Use next/image normally:
import Image from 'next/image'
export default function Page() {
return (
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
quality={75}
/>
)
}
vinext automatically generates optimization URLs:
<img
src="/_vinext/image?url=%2Fphoto.jpg&w=800&q=75"
srcset="
/_vinext/image?url=%2Fphoto.jpg&w=640&q=75 640w,
/_vinext/image?url=%2Fphoto.jpg&w=750&q=75 750w,
/_vinext/image?url=%2Fphoto.jpg&w=828&q=75 828w
"
width="800"
height="600"
alt="Photo"
/>
Remote Images
Remote images work via @unpic/react which auto-detects 28 CDN providers:
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://cdn.example.com/photo.jpg"
alt="Photo"
width={800}
height={600}
/>
)
}
Configure allowed patterns in next.config.js:
export default {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**'
}
]
}
}
The handler automatically serves the best format based on the Accept header:
- AVIF (smallest, newer browsers)
- WebP (smaller, wide support)
- JPEG (fallback)
Cloudflare’s edge cache stores one variant per format, so the same image URL can return different formats to different clients.
Security
Path Validation
Only path-relative URLs are allowed. This prevents:
- Open redirect:
?url=https://evil.com/phishing
- SSRF:
?url=http://internal-service/admin
- Protocol smuggling:
?url=data:image/svg+xml,...
Valid:
?url=/photo.jpg
?url=/images/profile.png
Invalid (returns 400):
?url=https://example.com/photo.jpg
?url=//example.com/photo.jpg
?url=data:image/svg+xml,...
Width/Quality Limits
The handler validates:
- Width: Must be
>= 0. 0 = no resize.
- Quality: Must be
1-100.
Invalid values return 400 Bad Request.
Fallback Behavior
If transformImage is omitted or throws an error, the handler serves the original image with cache headers:
return handleImageOptimization(request, {
fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url)))
// No transformImage — serves original
})
This is useful for:
- Development server (no Images binding)
- Non-Cloudflare deployments
- Debugging
Development vs Production
Development
In dev mode, images are served as passthroughs (no optimization). The transformImage callback is omitted:
if (url.pathname === '/_vinext/image') {
return handleImageOptimization(request, {
fetchAsset: (path) => {
// Serve from public/ directory
return fetch(new URL(path, 'http://localhost:5173'))
}
})
}
Production
In production on Workers, images are optimized via Cloudflare Images:
if (url.pathname === '/_vinext/image') {
return handleImageOptimization(request, {
fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))),
transformImage: async (body, { width, format, quality }) => {
const result = await env.IMAGES.input(body)
.transform(width > 0 ? { width } : {})
.output({ format, quality })
return result.response()
}
})
}
Example: Custom Watermarking
Add custom image processing logic:
if (url.pathname === '/_vinext/image') {
return handleImageOptimization(request, {
fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))),
transformImage: async (body, { width, format, quality }) => {
// Add watermark
const result = await env.IMAGES.input(body)
.transform(width > 0 ? { width } : {})
.draw([
{
url: 'https://example.com/watermark.png',
bottom: 10,
right: 10,
opacity: 0.5
}
])
.output({ format, quality })
return result.response()
}
})
}
See Cloudflare Images API for transform options.
Limitations
- No build-time optimization: Images are optimized at request time, not build time
- No static resizing:
next export doesn’t pre-generate responsive variants
- No blur placeholders:
placeholder="blur" is not supported
- No
sizes auto-detection: You must provide the sizes prop manually for responsive images