{"id":1604,"date":"2025-11-09T22:56:25","date_gmt":"2025-11-09T19:56:25","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/the-friendly-way-to-serve-webp-avif-without-breaking-your-site-or-your-seo\/"},"modified":"2025-11-09T22:56:25","modified_gmt":"2025-11-09T19:56:25","slug":"the-friendly-way-to-serve-webp-avif-without-breaking-your-site-or-your-seo","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/the-friendly-way-to-serve-webp-avif-without-breaking-your-site-or-your-seo\/","title":{"rendered":"The Friendly Way to Serve WebP\/AVIF Without Breaking Your Site (or Your SEO)"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, late one Tuesday night, staring at a waterfall chart and wondering why an image-heavy homepage still felt sluggish on a crisp fiber connection. I had rolled out WebP and AVIF earlier that week, feeling pretty smug about the savings. But something was off. Safari users were pinging me with \u201cimage downloads\u201d instead of actual images. The CDN cache hit ratio had fallen off a cliff. And Googlebot seemed to be fetching the same image over and over like it couldn\u2019t make up its mind. That\u2019s when it hit me: I had optimized the images, but not the delivery. I had forgotten the most boring, unglamorous part of the whole thing\u2014content negotiation, headers, and cache keys.<\/p>\n<p>Ever had that moment when your image optimization turns into a game of whack-a-mole? You ship WebP or AVIF and suddenly something breaks\u2014maybe it\u2019s the image preview in a chat app, or an older Android browser, or a CDN that starts caching every Accept header permutation as if it\u2019s a unique user. The trick isn\u2019t just converting images to modern formats; it\u2019s serving them smartly, without changing URLs, without leaking cache variants, and without confusing search engines.<\/p>\n<p>In this guide, I\u2019ll walk you through the friendly, no-drama way to serve WebP and AVIF with Nginx or Apache, make CDNs behave, and keep SEO tidy. We\u2019ll talk about the Accept header and Vary, rewrite rules that don\u2019t get you in trouble, predictable cache keys, and a conversion pipeline that won\u2019t cook your CPU. I\u2019ll also share a couple of war stories and the exact snippets I use on real sites. By the end, you\u2019ll know how to roll out next-gen images with confidence\u2014and sleep through the night after you hit deploy.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_WebPAVIF_Are_Awesomeand_How_They_Break_Stuff\"><span class=\"toc_number toc_depth_1\">1<\/span> Why WebP\/AVIF Are Awesome\u2014and How They Break Stuff<\/a><\/li><li><a href=\"#Content_Negotiation_Without_Tears_Accept_and_Vary\"><span class=\"toc_number toc_depth_1\">2<\/span> Content Negotiation Without Tears: Accept and Vary<\/a><\/li><li><a href=\"#The_Nginx_Recipe_Map_Try_Files_and_Stable_Cache_Keys\"><span class=\"toc_number toc_depth_1\">3<\/span> The Nginx Recipe: Map, Try Files, and Stable Cache Keys<\/a><ul><li><a href=\"#Step_1_Normalize_the_Accept_header\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Step 1: Normalize the Accept header<\/a><\/li><li><a href=\"#Step_2_Serve_the_best_available_file\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Step 2: Serve the best available file<\/a><\/li><li><a href=\"#Step_3_Keep_your_proxy_cache_calm\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Step 3: Keep your proxy cache calm<\/a><\/li><li><a href=\"#Testing_the_Nginx_setup\"><span class=\"toc_number toc_depth_2\">3.4<\/span> Testing the Nginx setup<\/a><\/li><\/ul><\/li><li><a href=\"#The_Apache_Recipe_Rewrite_Rules_That_Dont_Bite\"><span class=\"toc_number toc_depth_1\">4<\/span> The Apache Recipe: Rewrite Rules That Don\u2019t Bite<\/a><ul><li><a href=\"#Step_1_Teach_Apache_the_new_types\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Step 1: Teach Apache the new types<\/a><\/li><li><a href=\"#Step_2_Vary_on_Accept\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Step 2: Vary on Accept<\/a><\/li><li><a href=\"#Step_3_Rewrite_only_when_its_safe\"><span class=\"toc_number toc_depth_2\">4.3<\/span> Step 3: Rewrite only when it\u2019s safe<\/a><\/li><li><a href=\"#Cache_policy_and_ETag\"><span class=\"toc_number toc_depth_2\">4.4<\/span> Cache policy and ETag<\/a><\/li><\/ul><\/li><li><a href=\"#CDNs_Keep_the_Cache_Smart_Not_Fragile\"><span class=\"toc_number toc_depth_1\">5<\/span> CDNs: Keep the Cache Smart, Not Fragile<\/a><\/li><li><a href=\"#SEO-Safe_Image_Delivery_Keep_URLs_Stable_and_Bots_Happy\"><span class=\"toc_number toc_depth_1\">6<\/span> SEO-Safe Image Delivery: Keep URLs Stable and Bots Happy<\/a><\/li><li><a href=\"#A_Conversion_Pipeline_That_Wont_Melt_Your_Servers\"><span class=\"toc_number toc_depth_1\">7<\/span> A Conversion Pipeline That Won\u2019t Melt Your Servers<\/a><ul><li><a href=\"#A_tiny_pseudo-pipeline\"><span class=\"toc_number toc_depth_2\">7.1<\/span> A tiny pseudo-pipeline<\/a><\/li><\/ul><\/li><li><a href=\"#Putting_It_All_Together_With_CDNs_You_Already_Use\"><span class=\"toc_number toc_depth_1\">8<\/span> Putting It All Together With CDNs You Already Use<\/a><\/li><li><a href=\"#Troubleshooting_Quick_Checks_Before_You_Panic\"><span class=\"toc_number toc_depth_1\">9<\/span> Troubleshooting: Quick Checks Before You Panic<\/a><\/li><li><a href=\"#Do_You_Need_the_Picture_Element_Too\"><span class=\"toc_number toc_depth_1\">10<\/span> Do You Need the Picture Element Too?<\/a><\/li><li><a href=\"#A_Few_Edge_Cases_Ive_Actually_Seen\"><span class=\"toc_number toc_depth_1\">11<\/span> A Few Edge Cases I\u2019ve Actually Seen<\/a><\/li><li><a href=\"#Your_Calm_Repeatable_Checklist\"><span class=\"toc_number toc_depth_1\">12<\/span> Your Calm, Repeatable Checklist<\/a><\/li><li><a href=\"#Wrap-Up_Ship_It_Without_the_Stress\"><span class=\"toc_number toc_depth_1\">13<\/span> Wrap-Up: Ship It Without the Stress<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"Why_WebPAVIF_Are_Awesomeand_How_They_Break_Stuff\">Why WebP\/AVIF Are Awesome\u2014and How They Break Stuff<\/span><\/h2>\n<p>Let\u2019s start with the upside. AVIF and WebP can shrink your images dramatically, often without any visible loss in quality. That translates to fewer bytes over the wire, better LCP, and a smoother experience for anyone on a flaky mobile connection. But here\u2019s the thing\u2014images aren\u2019t just files; they\u2019re content with a story that involves browsers, proxies, CDNs, crawlers, and sometimes even email clients and chat previews. When you change how images are served, you\u2019re changing that story.<\/p>\n<p>I remember one rollout where we switched JPEGs to WebP using a blunt rewrite rule. It felt great in Chrome and Android\u2014blazing fast. Then the support messages trickled in: \u201cSafari is downloading files,\u201d \u201cPinterest can\u2019t pin images,\u201d and my favorite, \u201cSlack preview looks like it\u2019s from 2009.\u201d The root cause wasn\u2019t the format; it was mismatched headers and a CDN that didn\u2019t understand what we were doing. The Accept header said one thing, the cache said another, and we weren\u2019t telling intermediaries how to vary the response.<\/p>\n<p>That\u2019s the heart of it: <strong>serve the right image for the client, make the cache aware of your logic, and keep the URL stable<\/strong>. When you do those three things consistently, everything else falls into place.<\/p>\n<h2 id=\"section-2\"><span id=\"Content_Negotiation_Without_Tears_Accept_and_Vary\">Content Negotiation Without Tears: Accept and Vary<\/span><\/h2>\n<p>Think of the Accept header as a friendly handshake from the browser. It says, \u201cHey, here are the formats I can handle.\u201d Modern browsers send something like <strong>Accept: image\/avif,image\/webp,image\/apng,image\/*,*\/*;q=0.8<\/strong>. Older ones might stop at JPEG or PNG. Your job is to read that handshake and respond with the best you can serve, along with the right headers so caches don\u2019t get confused.<\/p>\n<p>Two headers matter here. The first is <strong>Content-Type<\/strong>. If you serve AVIF, say so with <strong>image\/avif<\/strong>. Same for WebP and the classics. The second is the big one: <strong>Vary: Accept<\/strong>. This tells caches to keep different variants of the same URL, based on what the client accepts. Without Vary: Accept, your CDN might cache a WebP response and hand it to a browser that only understands JPEG, which is where those weird \u201cdownload\u201d issues come from.<\/p>\n<p>There\u2019s a hidden wrinkle. Accept headers vary a lot. Some browsers include quality values; some don\u2019t. If you feed the raw Accept header into your cache key, you\u2019ll split the cache unnecessarily. You want to <strong>normalize<\/strong> the Accept header down to a simple state: avif, or webp, or original. That way, you avoid thousands of tiny cache buckets and stick to three neat variants.<\/p>\n<p>If you want to refresh your memory on Accept, the MDN page is a good read: <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Accept\" rel=\"nofollow noopener\" target=\"_blank\">how the Accept header works<\/a>. But don\u2019t worry\u2014we\u2019ll cook this into simple config you can drop into Nginx or Apache without getting lost in header theory.<\/p>\n<h2 id=\"section-3\"><span id=\"The_Nginx_Recipe_Map_Try_Files_and_Stable_Cache_Keys\">The Nginx Recipe: Map, Try Files, and Stable Cache Keys<\/span><\/h2>\n<p>I\u2019ve lost count of how many times I\u2019ve used this exact Nginx snippet. It\u2019s simple, predictable, and easy to reason about. The idea is to map the Accept header into a tidy variable, check for pre-generated AVIF and WebP files, and then serve the best match with the right headers. No 302 hops, no funky query strings, and no moving the goalposts for your sitemap.<\/p>\n<h3><span id=\"Step_1_Normalize_the_Accept_header\">Step 1: Normalize the Accept header<\/span><\/h3>\n<p>Use <strong>map<\/strong> to convert the noisy Accept header into a small set of states. This keeps your logic and cache keys clean. If you\u2019re curious why map is perfect for this, the official doc is short and sweet: <a href=\"https:\/\/nginx.org\/en\/docs\/http\/ngx_http_map_module.html\" rel=\"nofollow noopener\" target=\"_blank\">Nginx map directive<\/a>.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">map $http_accept $image_preference {\n    default   &quot;orig&quot;;\n    ~*avif    &quot;avif&quot;;\n    ~*webp    &quot;webp&quot;;\n}\n<\/code><\/pre>\n<h3><span id=\"Step_2_Serve_the_best_available_file\">Step 2: Serve the best available file<\/span><\/h3>\n<p>Assume you\u2019ve pre-generated image.avif and image.webp next to image.jpg. We\u2019ll try AVIF when $image_preference says so and the file exists, then WebP, then the original. We\u2019ll also set Vary: Accept and a strong cache policy. Adjust the extension list to your needs.<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">server {\n    # ... your other config ...\n\n    # add types for safety (if not already set globally)\n    types {\n        image\/avif avif;\n        image\/webp webp;\n    }\n\n    # normalize cache key input and help downstream caches\n    add_header Vary Accept;\n\n    # long cache for immutable, fingerprinted assets; adjust to your strategy\n    map $request_uri $img_cache_control {\n        default &quot;public, max-age=31536000, immutable&quot;;\n    }\n\n    location ~* .(?:jpg|jpeg|png)$ {\n        set $best &quot;$uri&quot;;\n\n        if ($image_preference = avif) {\n            if (-f $uri.avif) { set $best &quot;$uri.avif&quot;; }\n        }\n        if ($image_preference = webp) {\n            if (-f $uri.webp) { set $best &quot;$uri.webp&quot;; }\n        }\n\n        try_files $best =404;\n        add_header Cache-Control $img_cache_control;\n    }\n}\n<\/code><\/pre>\n<p>This approach serves the same URL path, just with a different underlying file if the client supports it. The browser gets a proper Content-Type, your cache becomes variant-aware thanks to Vary: Accept, and you dodge the whole \u201credirect my .jpg to .webp\u201d risk that tends to confuse crawlers and chat scrapers.<\/p>\n<h3><span id=\"Step_3_Keep_your_proxy_cache_calm\">Step 3: Keep your proxy cache calm<\/span><\/h3>\n<p>If you\u2019re using Nginx caching or a reverse proxy in front, extend the cache key with the normalized preference rather than the raw Accept header. That reduces cache fragmentation and keeps hit rates healthy.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># example for proxy_cache_key\nproxy_cache_key &quot;$scheme$request_method$host$request_uri:$image_preference&quot;;\n<\/code><\/pre>\n<p>In practice, this gives you at most three variants: avif, webp, or orig. Clean, predictable, and easy to purge if you ever need to.<\/p>\n<h3><span id=\"Testing_the_Nginx_setup\">Testing the Nginx setup<\/span><\/h3>\n<p>Use curl to simulate different clients. Hit the same URL and compare headers.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># AVIF-capable client\ncurl -I -H &quot;Accept: image\/avif,image\/webp&quot; https:\/\/example.com\/images\/hero.jpg\n\n# WebP-only client\ncurl -I -H &quot;Accept: image\/webp&quot; https:\/\/example.com\/images\/hero.jpg\n\n# Legacy client\ncurl -I -H &quot;Accept: image\/jpeg&quot; https:\/\/example.com\/images\/hero.jpg\n<\/code><\/pre>\n<p>Make sure you get image\/avif, image\/webp, or image\/jpeg Content-Type as expected, along with Vary: Accept and a sane Cache-Control. If that looks good, open the page in Chrome, Safari, and Firefox and confirm you aren\u2019t seeing downloads or MIME errors in the console.<\/p>\n<h2 id=\"section-4\"><span id=\"The_Apache_Recipe_Rewrite_Rules_That_Dont_Bite\">The Apache Recipe: Rewrite Rules That Don\u2019t Bite<\/span><\/h2>\n<p>Apache can do this just as gracefully. The trap people fall into is aggressive rewriting without checking for file existence or setting Vary. You don\u2019t need fancy modules here\u2014mod_rewrite, mod_mime, mod_headers, and you\u2019re golden.<\/p>\n<h3><span id=\"Step_1_Teach_Apache_the_new_types\">Step 1: Teach Apache the new types<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;IfModule mod_mime.c&gt;\n    AddType image\/avif .avif\n    AddType image\/webp .webp\n&lt;\/IfModule&gt;\n<\/code><\/pre>\n<h3><span id=\"Step_2_Vary_on_Accept\">Step 2: Vary on Accept<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;IfModule mod_headers.c&gt;\n    Header append Vary Accept\n&lt;\/IfModule&gt;\n<\/code><\/pre>\n<h3><span id=\"Step_3_Rewrite_only_when_its_safe\">Step 3: Rewrite only when it\u2019s safe<\/span><\/h3>\n<p>This snippet checks what the client accepts, then prefers AVIF, then WebP, and finally falls back to the original. It also verifies that the alternative file exists before rewriting.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;IfModule mod_rewrite.c&gt;\nRewriteEngine On\n\n# Only trigger on common raster formats\nRewriteCond %{REQUEST_URI} .(jpe?g|png)$ [NC]\n\n# Prefer AVIF if accepted and file exists\nRewriteCond %{HTTP_ACCEPT} image\/avif [OR]\nRewriteCond %{HTTP_ACCEPT} image\/* [NC]\nRewriteCond %{REQUEST_FILENAME}.avif -f\nRewriteRule ^(.+).(jpe?g|png)$ $1.$2.avif [T=image\/avif,E=img_ext:avif,L]\n\n# Otherwise try WebP\nRewriteCond %{REQUEST_URI} .(jpe?g|png)$ [NC]\nRewriteCond %{HTTP_ACCEPT} image\/webp [OR]\nRewriteCond %{HTTP_ACCEPT} image\/* [NC]\nRewriteCond %{REQUEST_FILENAME}.webp -f\nRewriteRule ^(.+).(jpe?g|png)$ $1.$2.webp [T=image\/webp,E=img_ext:webp,L]\n&lt;\/IfModule&gt;\n<\/code><\/pre>\n<p>Depending on your Apache version, you might prefer controlling this in the vhost rather than .htaccess for performance. If you ever forget the right flags, the official <a href=\"https:\/\/httpd.apache.org\/docs\/current\/rewrite\/\" rel=\"nofollow noopener\" target=\"_blank\">mod_rewrite documentation<\/a> is surprisingly approachable once you\u2019ve had coffee.<\/p>\n<h3><span id=\"Cache_policy_and_ETag\">Cache policy and ETag<\/span><\/h3>\n<p>For fingerprinted images (like hero.abcd1234.jpg), set Cache-Control to a long max-age and optionally immutable. For non-fingerprinted assets, lean on ETag or Last-Modified. One small tip: if you use ETag, don\u2019t reuse the same ETag across variants. I like including a subtle prefix per variant so a 304 response truly matches the bytes the client expects.<\/p>\n<h2 id=\"section-5\"><span id=\"CDNs_Keep_the_Cache_Smart_Not_Fragile\">CDNs: Keep the Cache Smart, Not Fragile<\/span><\/h2>\n<p>CDNs are where image optimization can shine\u2014or fall apart. You\u2019ll want the CDN to cache per variant, but only across three tidy states (avif, webp, orig). If you let the CDN key the cache off the raw Accept header, you can end up with dozens of variants for the same URL and a hit ratio that craters.<\/p>\n<p>The playbook I keep returning to looks like this. First, normalize Accept at the edge or at origin. Second, include that normalized state in the cache key. Third, always send <strong>Vary: Accept<\/strong> from origin so every layer understands what you\u2019re doing. And finally, avoid redirects. Respond with a 200 and the right Content-Type.<\/p>\n<p>On CDNs that support header-based cache keys or edge logic, create a small function that translates Accept to a single string: avif if \u201cimage\/avif\u201d appears, webp if \u201cimage\/webp\u201d appears, otherwise orig. Put that string into the cache key. If your CDN doesn\u2019t allow edge logic, do the mapping at origin (like we did with Nginx map) and forward that normalized value as a custom header that the CDN can key on, or include it in the cache key via a rule.<\/p>\n<p>One last tip from a gnarly incident: some CDNs aggressively canonicalize headers in unexpected ways. After flipping the switch, I always run a quick test\u2014fetch the same URL five or six times with different Accept headers and watch the CDN response headers for cache hits and misses. If you see random misses across the same two or three patterns, your key isn\u2019t normalized enough.<\/p>\n<h2 id=\"section-6\"><span id=\"SEO-Safe_Image_Delivery_Keep_URLs_Stable_and_Bots_Happy\">SEO-Safe Image Delivery: Keep URLs Stable and Bots Happy<\/span><\/h2>\n<p>SEO for images gets overthought sometimes. The core principle is simple: <strong>don\u2019t move the goalposts<\/strong>. Keep the original URL stable. Serve the best bytes for the client with content negotiation. And resist the urge to redirect your .jpg to a .webp or .avif URL. Redirects introduce a detour and sometimes confuse tools that expect a \u201c.jpg\u201d to be, well, a JPEG file.<\/p>\n<p>Here\u2019s what has worked for me across a bunch of sites. First, always return the correct <strong>Content-Type<\/strong> for whatever you send, even if the URL ends in .jpg. Browsers care about headers more than extensions. Second, set <strong>Vary: Accept<\/strong>. Crawlers that support modern formats will fetch the modern bytes, and those that don\u2019t will happily take the original. Third, keep your image sitemaps and internal links pointing to the canonical image URL. No need to generate separate sitemaps for .webp or .avif. Fourth, if you use the <strong>picture<\/strong> element in HTML, ensure the <em>img<\/em> fallback is a standard JPEG or PNG, not a modern-only format. That way even a very old client sees a real image.<\/p>\n<p>One thing I learned the hard way: some chat apps and email clients fetch with minimal headers and poor MIME handling. If they hit a CDN that returns a cached WebP to a client that doesn\u2019t understand it, you\u2019ll see broken previews. That\u2019s why normalizing the cache key and including Vary: Accept is non-negotiable. It protects both modern and legacy clients, and it keeps previews from going sideways when someone shares your link.<\/p>\n<h2 id=\"section-7\"><span id=\"A_Conversion_Pipeline_That_Wont_Melt_Your_Servers\">A Conversion Pipeline That Won\u2019t Melt Your Servers<\/span><\/h2>\n<p>Let\u2019s talk about actually producing the AVIF and WebP files. If you try to convert everything on the fly, you\u2019ll quickly get into CPU-hungry territory. My rule is to pre-generate variants at build time or asynchronously on upload, then keep them next to the originals. This pairs beautifully with the Nginx and Apache rules from above. When the server checks for image.jpg.avif, it finds it instantly and serves it without touching the CPU-heavy path.<\/p>\n<p>For AVIF and WebP, I\u2019ve had great luck with libvips-based tooling and well-tuned encoders. AVIF can sometimes introduce banding or subtly crush gradients if you\u2019re too aggressive with quality; WebP is more forgiving and still delivers big wins. For logos and UI elements with hard edges, consider lossless or near-lossless settings. For photos, I like a single sane default and a cap on dimensions to avoid shipping billboard-sized images to mobile clients.<\/p>\n<p>I once helped a publisher migrate their entire photo library. We batched it overnight with a queue worker that throttled concurrency based on CPU pressure. If the server got warm, the queue slowed down; if it was quiet, it sped up. No drama, and no 3 a.m. alarms from a CPU pegged at 100%.<\/p>\n<h3><span id=\"A_tiny_pseudo-pipeline\">A tiny pseudo-pipeline<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># 1) On upload or in CI, resize to sane variants (e.g., 320\/640\/1280\/1920)\n# 2) For each size, produce .avif and .webp next to the original\n# 3) Store them with the same basename so rewrites can find them quickly\n\n# Example with cwebp and avifenc (tune to your taste)\n# webp\ncwebp -q 78 input.jpg -o input.jpg.webp\n\n# avif\navifenc --min 20 --max 32 --cq-level 28 --jobs 4 input.jpg input.jpg.avif\n\n# For logos or UI: consider lossless\/near-lossless where it makes sense\n<\/code><\/pre>\n<p>If you want a bigger-picture walk-through about costs and cache keys beyond just the server rules, I wrote a deeper dive on <a href=\"https:\/\/www.dchost.com\/blog\/en\/goruntu-optimizasyonu-boru-hatti-nasil-kurulur-avif-webp-origin-shield-ve-akilli-cache-key-ile-cdn-faturaniza-nefes-aldirin\/\">building an image optimization pipeline with AVIF\/WebP, origin shield, and smarter cache keys<\/a>. It\u2019s the calm approach I keep reusing when teams are worried about CDN bills and surprise traffic spikes.<\/p>\n<h2 id=\"section-8\"><span id=\"Putting_It_All_Together_With_CDNs_You_Already_Use\">Putting It All Together With CDNs You Already Use<\/span><\/h2>\n<p>Not every CDN exposes the same knobs, but the principles stay the same. If your CDN supports cache policy rules, include the Accept header or your normalized variant in the cache key. If it supports request transforms, normalize Accept there. If none of that is available, do the heavy lifting at origin and forward a small custom header like X-Image-Preference with values avif, webp, or orig; then tell the CDN to include that header in the cache key.<\/p>\n<p>Another pattern I\u2019ve used is origin-driven negotiation with edge hints. The origin decides which variant to serve, sets Vary: Accept, and the CDN quietly caches the response per key. This avoids complex VCL or edge scripting and keeps your mental model simple: the origin is the brain, the CDN is the brawn.<\/p>\n<p>One caveat I want to share from a client build: if you have a multi-CDN setup or a mix of static hosting and reverse proxying, watch for header stripping. I\u2019ve seen a layer drop Vary: Accept without telling anyone, leading to strange mismatches downstream. After rollout, run a quick crawl across key pages and confirm the headers are intact at the final hop, not just at origin.<\/p>\n<h2 id=\"section-9\"><span id=\"Troubleshooting_Quick_Checks_Before_You_Panic\">Troubleshooting: Quick Checks Before You Panic<\/span><\/h2>\n<p>When something feels off, I run the same playbook. I hit a single image URL with curl using different Accept headers and check the Content-Type, Vary, Cache-Control, and ETag. If any of those are missing or surprising, I fix that first. Then I watch the CDN response headers to confirm I\u2019m hitting the cache for repeat requests. Finally, I open the page in multiple browsers and see if the network panel lines up with expectations. It sounds basic, but nine times out of ten, the boring checks catch the sneaky bugs.<\/p>\n<p>Here are a few quick commands I keep handy:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># See exactly what the server is sending\ncurl -I https:\/\/example.com\/images\/hero.jpg\n\n# Simulate an AVIF-capable client\ncurl -I -H &quot;Accept: image\/avif,image\/webp&quot; https:\/\/example.com\/images\/hero.jpg\n\n# Force a legacy client\ncurl -I -H &quot;Accept: image\/jpeg&quot; https:\/\/example.com\/images\/hero.jpg\n<\/code><\/pre>\n<p>If you see a WebP or AVIF Content-Type without Vary: Accept, that\u2019s your first fix. If you see wildly different Cache-Control headers between variants, unify them. If the CDN is missing cache hits between identical requests, tighten your cache key.<\/p>\n<h2 id=\"section-10\"><span id=\"Do_You_Need_the_Picture_Element_Too\">Do You Need the Picture Element Too?<\/span><\/h2>\n<p>Server-side negotiation is fantastic because it keeps URLs stable and requires no template changes. That said, the <strong>picture<\/strong> element still shines for responsive art direction and when you want clear, explicit control over which formats a client sees. Think of picture as the steering wheel and server negotiation as cruise control. You can use both. Just make sure your <em>img<\/em> fallback is a classic format so truly old clients aren\u2019t left out.<\/p>\n<p>On sites with tight rendering budgets, I\u2019ll usually start with server negotiation for simplicity, then sprinkle in picture for hero images or places where we want to adjust composition between mobile and desktop. It\u2019s a nice balance\u2014fast by default, precise where it matters.<\/p>\n<h2 id=\"section-11\"><span id=\"A_Few_Edge_Cases_Ive_Actually_Seen\">A Few Edge Cases I\u2019ve Actually Seen<\/span><\/h2>\n<p>Two odd issues that might save you an hour someday. First, I once saw a third-party proxy that insisted on sniffing bytes to guess the type, then injected its own Content-Type. Pair that with a non-varied cache and you get chaos. The fix was to lock headers down at origin and enforce Vary strictly through every layer. Second, we hit a \u201cdownload the image\u201d bug in an older Safari on macOS where the CDN had cached a WebP. Safari asked politely for JPEG, but the CDN served the WebP anyway due to a bad cache key. That one taught me to never, ever skip the Vary: Accept header on image responses. One tiny header; a world of calm.<\/p>\n<h2 id=\"section-12\"><span id=\"Your_Calm_Repeatable_Checklist\">Your Calm, Repeatable Checklist<\/span><\/h2>\n<p>When I roll this out now, I follow the same rhythm. Normalize Accept into three states. At origin, try AVIF, then WebP, then the original. Send Vary: Accept and make sure Content-Type matches bytes. Use long-lived cache headers for fingerprinted assets. For CDNs, include the normalized state in the cache key. Test with curl and with real browsers. And never redirect .jpg to .webp\u2014it\u2019s a shortcut that usually leads to more work later.<\/p>\n<p>If you\u2019re also fighting with broader caching strategies around CSS and JS, I wrote a separate piece on immutable assets, ETag vs. Last-Modified, and fingerprinting that pairs well with this approach. It\u2019s here if you want to go deeper: <a href=\"https:\/\/www.dchost.com\/blog\/en\/nereden-baslamaliyiz-bir-css-dosyasinin-pesinde\/\">Stop Fighting Your Cache: The Friendly Guide to Cache-Control immutable, ETag vs Last-Modified, and Asset Fingerprinting<\/a>. And if you ever want to revisit the basics of Accept headers or rewrite rules, the docs are simple enough when taken one sip at a time: MDN on <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Accept\" rel=\"nofollow noopener\" target=\"_blank\">Accept<\/a>, Nginx <a href=\"https:\/\/nginx.org\/en\/docs\/http\/ngx_http_map_module.html\" rel=\"nofollow noopener\" target=\"_blank\">map<\/a>, and Apache <a href=\"https:\/\/httpd.apache.org\/docs\/current\/rewrite\/\" rel=\"nofollow noopener\" target=\"_blank\">mod_rewrite<\/a>.<\/p>\n<h2 id=\"section-13\"><span id=\"Wrap-Up_Ship_It_Without_the_Stress\">Wrap-Up: Ship It Without the Stress<\/span><\/h2>\n<p>If you\u2019ve made it this far, you\u2019ve basically got the whole play. Serving WebP and AVIF without breaking things isn\u2019t about fancy image magic\u2014it\u2019s about respectful negotiation with the browser, tidy cache keys, and headers that tell the truth. Keep the URL stable. Prefer AVIF, then WebP, then the original. Set Vary: Accept every time. Normalize the Accept header so your CDN cache stays healthy. And test like a detective for five minutes before you ship.<\/p>\n<p>My favorite part is how quietly this all works once it\u2019s set up. You don\u2019t need to brag about it in your templates or paint the UI with new classes. The server does the right thing, the CDN does the efficient thing, and your users just get faster pages without noticing. That\u2019s the good kind of invisible.<\/p>\n<p>If you\u2019re the type who enjoys building things once and letting them run, you might also like the deeper dive on <a href=\"https:\/\/www.dchost.com\/blog\/en\/goruntu-optimizasyonu-boru-hatti-nasil-kurulur-avif-webp-origin-shield-ve-akilli-cache-key-ile-cdn-faturaniza-nefes-aldirin\/\">image optimization pipelines and smarter cache keys<\/a>. Either way, I hope this gave you a calm path forward. Have fun rolling it out, and if you catch any quirky edge cases, drop me a note\u2014I\u2019ve probably seen a cousin of it somewhere. Hope this was helpful! See you in the next post.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, late one Tuesday night, staring at a waterfall chart and wondering why an image-heavy homepage still felt sluggish on a crisp fiber connection. I had rolled out WebP and AVIF earlier that week, feeling pretty smug about the savings. But something was off. Safari users were pinging me with \u201cimage downloads\u201d [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1605,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1604","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1604","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1604"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1604\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1605"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1604"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1604"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1604"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}