bext.dev
DocsGuides
Guides4 min read
On this page

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.

toml
[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:

code
/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#

bash
# 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:

html
<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_optimization plugin.

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:

html
<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:

code
{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=640 saves significant bandwidth. Keep allowed_widths tight 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/image URLs after a restart to prime the in-memory cache.
  • Tune quality -- for hero images, q=80 is a good default. For thumbnails, q=60 is often indistinguishable and significantly smaller.
Warning

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.

Tip

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.

  • 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.toml field listing
  • Compression — Brotli/gzip for text assets; images use this plugin instead
Edit this page ↗Need a hand? ↗
FIND YOUR NEXT STEP

Start with a topic, a command, or a question.