Image Optimization
bext can optimize images on-the-fly with zero build-time preprocessing. The
image_optimization plugin exposes an /api/image endpoint that fetches a
remote source image, resizes it to a requested width, recompresses it, and
serves the result from an in-memory cache. Original files are never modified.
Configuration#
Enable and tune the plugin in the [plugins.image_optimization] section of
bext.config.toml. The plugin is off by default; set enabled = true to
register the /api/image route.
[plugins.image_optimization]
enabled = true
# Widths a client may request via `?w=`. A request for any other width is
# rejected with 400. (Default shown below.)
allowed_widths = [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840]
default_quality = 75 # Quality (1-100) used when `?q=` is omitted
max_source_bytes = 10485760 # Reject source images larger than this (default 10 MiB)
cache_max_entries = 5000 # Max optimized variants held in the in-memory LRU
cache_ttl_ms = 86400000 # Cache entry TTL in milliseconds (default 24h)
All fields are optional and fall back to the defaults shown above
(enabled defaults to false).
Query Parameter API#
The /api/image endpoint takes the source image as a url query parameter and
the transform options alongside it:
/api/image?url=https%3A%2F%2Fexample.com%2Fhero.jpg&w=800&q=75
| Parameter | Description | Default |
|---|---|---|
url |
URL-encoded source image URL (required, HTTPS only) | — |
w |
Target width in pixels — must be a value in allowed_widths (required) |
— |
q |
Quality (1-100) | default_quality (75) |
The source URL must be https:// and must not resolve to a private or loopback
host (SSRF protection); the optimizer also follows no redirects. Requests that
omit url, omit w, or use a width outside allowed_widths return 400.
Examples#
# Resize a remote image to 640px wide at quality 75 (output format negotiated
# from the Accept header — see below)
curl 'https://example.com/api/image?url=https%3A%2F%2Fcdn.example.com%2Fphoto.jpg&w=640&q=75'
Format Negotiation#
The plugin reads the request Accept header and picks the output format
automatically — there is no format query parameter. The preference order is
WebP, then PNG (only if the client advertises image/png but not WebP),
otherwise JPEG:
| Accept header contains | Served format |
|---|---|
image/webp |
WebP |
image/png (and not WebP) |
PNG |
| Neither | JPEG (fallback) |
This means you can point a single <img> at /api/image and let bext choose
the format:
<img src="/api/image?url=https%3A%2F%2Fcdn.example.com%2Fhero.jpg&w=800" alt="Hero image" />
<!-- Browsers that advertise WebP receive WebP; others receive JPEG -->
Note: this plugin negotiates WebP / PNG / JPEG. AVIF is not produced by the
image_optimizationplugin.
Srcset Generation#
For responsive images, build a srcset by pointing each candidate at a
different ?w= value (each width must be in allowed_widths). Every entry
resolves to an independently-optimized image:
<img
srcset="/api/image?url=https%3A%2F%2Fcdn.example.com%2Fhero.jpg&w=640 640w,
/api/image?url=https%3A%2F%2Fcdn.example.com%2Fhero.jpg&w=1080 1080w,
/api/image?url=https%3A%2F%2Fcdn.example.com%2Fhero.jpg&w=1920 1920w"
sizes="(max-width: 640px) 100vw, 50vw"
src="/api/image?url=https%3A%2F%2Fcdn.example.com%2Fhero.jpg&w=1080"
alt="Hero image"
loading="lazy"
/>
Cache Behavior#
Optimized variants are held in an in-memory LRU cache, bounded by
cache_max_entries and expired after cache_ttl_ms. A cache hit returns
immediately and the response carries x-bext-image-cache: hit. Successful
responses are sent with cache-control: public, max-age=86400, immutable so
downstream CDNs and browsers cache them too.
The cache key is derived from a hash of the source URL combined with the width, quality, and negotiated format:
{xxhash64_hex}/{width}/q{quality}.{ext}
# Example: a1b2c3d4e5f67890/800/q75.webp
Performance Tips#
- Set explicit widths -- avoid serving the original 4000px image to mobile
clients. A
?w=640saves significant bandwidth. Keepallowed_widthstight to the sizes you actually use. - Rely on WebP -- the plugin serves WebP automatically to clients that advertise it, typically 25-35% smaller than JPEG at equivalent quality.
- Pre-warm popular images -- request popular
/api/imageURLs after a restart to prime the in-memory cache. - Tune quality -- for hero images,
q=80is a good default. For thumbnails,q=60is often indistinguishable and significantly smaller.
The plugin is off by default (enabled = false). Without enabled = true
in [plugins.image_optimization], the /api/image route is never registered
and all requests return 404.
The in-memory LRU cache resets on server restart. If you rely on image
optimization for high-traffic routes, use cache::warm (see the
Task Scheduler) to pre-fill the cache after a deploy.
Related#
- Caching — how the shared tiered cache works alongside the image LRU
- SEO Utilities — OG image generation uses the same optimization pipeline
- Task Scheduler — schedule post-deploy cache warming for popular image URLs
- Configuration reference — full
bext.config.tomlfield listing - Compression — Brotli/gzip for text assets; images use this plugin instead