How Server-Side Format Negotiation Works (and Why It Beats Client-Side Tricks)
When you decide to serve WebP and AVIF on your WordPress site, the first question most guides answer is how to convert your images. The second — and more important — question is: how does the right format actually reach the right browser?
There are two fundamentally different answers to that question. The client-side approach puts the format decision in your HTML. The server-side approach puts it in your web server configuration. They produce the same visual result, but they are not equivalent in terms of maintenance burden, reliability, or scope of coverage.
This article explains how server-side format negotiation works at a technical level, where client-side methods fall short, and how KuDesign ImageIO implements the server-side approach on WordPress without requiring you to write a single line of code.
How Browsers Announce What They Support
Every time a browser requests an image, it sends an HTTP request. That request includes a header called Accept, which lists the content types the browser is capable of displaying — in order of preference.
A current Chrome browser sends this for image requests:
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8Firefox sends:
Accept: image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5Safari (current versions) sends:
Accept: image/webp,image/avif,image/jxl,image/heic,image/heic-sequence,video/*,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5An older browser that supports neither AVIF nor WebP sends only:
Accept: image/png,image/*,*/*;q=0.5This header is sent automatically by the browser with every image request. The browser isn't asking you to serve a specific format — it's advertising its own capabilities and leaving the format decision to whoever is on the other end.
Server-side format negotiation means your web server reads this Accept header and responds with the best format the browser can handle — without any changes to your HTML.
The Server-Side Mechanism: How It Works
The pattern is straightforward. When an image request arrives:
- The server reads the
Acceptheader from the request - It checks whether a converted version of the requested file exists on disk
- It serves the best available format the browser supports
- It adds a
Vary: Acceptheader to the response so caches know this resource has multiple variants
Here's what this looks like in practice for an Nginx configuration:
http {
map $http_accept $avif_suffix {
default "";
"~image/avif" ".avif";
}
map $http_accept $webp_suffix {
default "";
"~image/webp" ".webp";
}
}
server {
location ~ ^/wp-content/uploads/.*\.(jpe?g|png)$ {
add_header Vary Accept;
try_files
$uri$avif_suffix
$uri$webp_suffix
$uri
=404;
}
}When a Chrome browser requests /wp-content/uploads/2026/06/product.jpg:
$avif_suffix=.avif(Chrome supports AVIF)$webp_suffix=.webp(Chrome supports WebP)- Nginx tries:
product.jpg.avif→ found → serves AVIF
When an older browser requests the same URL:
$avif_suffix= `` (not supported)$webp_suffix= `` (not supported)- Nginx tries:
product.jpg→ serves original JPEG
The URL in the browser's address bar never changes. The <img> tag in your HTML never changes. The visitor never knows which format they received — they just see the image load faster.
Apache achieves the same result with mod_rewrite rules in .htaccess:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_ACCEPT} image/avif
RewriteCond %{REQUEST_FILENAME} \.(jpe?g|png)$
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads/imageio/avif%{REQUEST_URI} -f
RewriteRule ^ /wp-content/uploads/imageio/avif%{REQUEST_URI} [L,T=image/avif]
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME} \.(jpe?g|png)$
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads/imageio/webp%{REQUEST_URI} -f
RewriteRule ^ /wp-content/uploads/imageio/webp%{REQUEST_URI} [L,T=image/webp]
</IfModule>The logic is identical: check the Accept header, check if the converted file exists, serve it. Fall through to the original if neither condition is met.
The Vary: Accept Header: Why It Matters for Caching
One detail that's easy to miss — and that causes real problems when skipped — is the Vary: Accept response header.
When a CDN or proxy cache stores a response, it uses the request URL as the cache key by default. Without Vary: Accept, a CDN might cache the AVIF version served to the first Chrome visitor and then serve that AVIF file to every subsequent visitor at the same URL — including browsers that don't support AVIF.
Vary: Accept tells caches: "this response varies based on the Accept header — cache separate variants for different Accept values." A CDN receiving this instruction will store the AVIF version and the JPEG version separately, and serve the appropriate one based on each incoming request's Accept header.
This is not optional if you're running a CDN or any caching layer in front of your WordPress origin server.
Why Client-Side Approaches Fall Short
The <picture> element is the HTML-native way to serve multiple formats:
<picture>
<source srcset="product.avif" type="image/avif">
<source srcset="product.webp" type="image/webp">
<img src="product.jpg" alt="Product image">
</picture>This works correctly. Browsers that support AVIF download the AVIF file. Browsers that support only WebP download the WebP. Older browsers get the JPEG. The format negotiation happens in the browser itself, before any request is made to the server.
So why prefer server-side negotiation? Several reasons.
It doesn't scale to an existing media library
If you have 2,000 images already on your WordPress site, using <picture> means updating every <img> tag across every post, page, and theme template. Even with a migration script, you'd need to regenerate every piece of content. For a site with years of existing posts, this is genuinely impractical.
Server-side negotiation requires no HTML changes at all. Existing content automatically benefits the moment the server rules are in place.
It doesn't cover dynamically generated markup
WordPress generates image tags through many different code paths — the block editor, classic editor, theme templates, page builders, plugin shortcodes, widget areas, REST API responses. A <picture> wrapper applied to one code path won't cover the others. Server-side negotiation intercepts at the HTTP layer, covering every image URL regardless of how the markup was generated.
It doesn't cover non-HTML contexts
Images aren't only loaded from <img> tags. CSS background-image properties, Open Graph meta tags (og:image), JSON API responses used by JavaScript frameworks, RSS feeds, sitemap image entries — none of these use <picture> elements. Server-side negotiation applies uniformly to all image requests, regardless of context.
JavaScript lazy loaders and plugins can interfere
Many WordPress sites use JavaScript-based lazy loading, page builders that manipulate the DOM, or optimization plugins that rewrite image tags. These can strip or ignore <picture> wrappers in unpredictable ways. Server-side negotiation has no dependency on JavaScript and isn't affected by DOM manipulation.
It adds markup complexity
Every image that needs format negotiation requires a <picture> wrapper with two <source> elements plus the fallback <img>. This triples the markup for every image, increases template complexity, and creates a maintenance surface. Server-side rules are written once and apply globally.
Where Client-Side Has a Genuine Advantage
Server-side negotiation isn't the right answer in every situation. The <picture> element is the correct tool when:
- You need art direction — serving a different crop or composition for mobile vs. desktop, not just a different format of the same image
- You control every image in the markup — a small landing page or campaign site where
<picture>tags can be written deliberately for each image - You're on a static site or CDN without server configuration access — when you can't modify
.htaccessor Nginx config,<picture>is your only option - You need per-image quality tuning — specifying different quality settings for AVIF vs. WebP versions of specific images
For WordPress sites with large existing media libraries, dynamic content generation, and diverse plugin ecosystems, server-side negotiation is the more practical and comprehensive solution.
How KuDesign ImageIO Implements This on WordPress
KuDesign ImageIO handles both sides of the equation: generating the converted files and configuring the server to serve them.
On the conversion side, the plugin scans your WordPress media library and converts JPEG and PNG files to WebP and AVIF using the image libraries available on your server (GD or Imagick). Converted files are stored separately under /wp-content/uploads/imageio/webp/ and /wp-content/uploads/imageio/avif/, with the same directory structure as the originals. Your source files are never modified.
On the server configuration side, the plugin's Server Config tab provides ready-to-copy Apache and Nginx rules for your specific directory layout. Paste them into your .htaccess (Apache) or server block (Nginx), reload your server, and format negotiation is active.
Verification is built in. The Server Config tab includes a live preview test that sends a request with the appropriate Accept header and reports back which format your server actually returned — PNG, WebP, or AVIF. This removes the guesswork from confirming the setup is working correctly.
New uploads are handled automatically. After the initial bulk conversion, new images uploaded to the media library are queued and converted in the background via WP-Cron. The server rules are already in place, so new images are served in modern formats as soon as their converted versions are available.
The result: a site that serves AVIF to Chrome and Firefox, WebP to older Safari and other compatible browsers, and JPEG/PNG to legacy browsers — all from the same image URLs, with no HTML changes, no JavaScript dependencies, and no per-image configuration.
Summary
Server-side format negotiation works by reading the browser's Accept header — a built-in HTTP mechanism that browsers have used for decades — and responding with the lightest format the browser can display. The URL stays the same. The HTML stays the same. The server does the work.
Compared to client-side approaches like <picture>:
Server-side (Accept header) | Client-side (<picture>) | |
|---|---|---|
| HTML changes required | None | Every image tag |
| Covers existing content | Yes | No |
| Covers CSS backgrounds | Yes | No |
| Covers API / RSS / OG images | Yes | No |
| JavaScript dependency | None | Possible (lazy loaders) |
| Art direction support | No | Yes |
| Setup effort | One-time server config | Per-image markup |
For WordPress sites with existing content, multiple plugins, and dynamic image generation, the server-side approach is more comprehensive, more maintainable, and requires less ongoing work. It's the approach KuDesign ImageIO is built around.
KuDesign ImageIO is a free plugin by KuDesign Limited, available on the [WordPress plugin directory](https://wordpress.org/plugins/kudesign-image-io/). It converts your media library to WebP and AVIF and provides server configuration rules to serve each format automatically based on browser capability.