Teknoloji

WebP/AVIF’i Kırmadan Sunmak: Nginx/Apache ve CDN ile İçerik Pazarlığı, Rewrite Kuralları ve SEO Uyumlu Dönüşüm

Bir Görsel Yüzünden Kopan Fırtına: Hiç Başınıza Geldi mi?

Bir sabah kahvemi alıp siteyi açtım, ana sayfadaki kahraman görseli pıt diye yok olmuş. Masaüstünde var, telefonda yok. Garip olan şu; dosya sunucuda duruyor, CDN’de cache dolu, izleme panelleri yeşil. Meğer mobil tarayıcı AVIF istiyor, CDN WebP cache’ini ısıtıp herkese dağıtıyor, sunucu da “tamamdır” diye başını sallıyor. Sonuç? Bazı cihazlarda doğru format, bazılarında boş kutu. O gün anladım ki, WebP/AVIF’i hızlı sunmak ayrı, kırmadan sunmak bambaşka bir konu.

Hiç benzer bir şey yaşadınız mı? Görsellerinizi yeni nesil formata çevirdiniz, her şey hızlı açılıyor, derken bir cihazda bozuldu, başka bir cihazda çalıştı, CDN ise eski varyantı sakladı. Böyle durumlarda asıl mesele tek tek formatlar değil; tarayıcıyla içerik pazarlığı, sunucu tarafı rewrite ve cache anahtarı düzeni. Bu yazıda tam da bunu konuşacağız. Adım adım, Nginx ve Apache tarafında kırmadan dönüştürmeyi, CDN’de cache karışıklığını engellemeyi ve SEO’dan ödün vermeden nasıl ilerleyeceğinizi anlatacağım.

Mesela şöyle düşünün: Aynı URL’den, tarayıcının kabul ettiğine göre AVIF ya da WebP veya klasik JPEG dönmek istiyorsunuz. İşin büyüsü Accept başlığında ve doğru Vary kullanımında gizli. Kod parçalarıyla, küçük hikayelerle, canlı ortamda başıma gelenleri örnek vererek gideceğiz. Hazırsanız, tarayıcıyla zarif bir anlaşma yapıp görselleri uçurmanın derdine düşelim.

İçerik Pazarlığı Nedir ve Neden Bu Kadar Önemli?

Tarayıcıyla konuşmanın en pratik yolu, istek başlıkları. Özellikle de Accept. Tarayıcı “image/avif” ya da “image/webp” diyerek neyi yuttuğunu söylüyor. Sunucu ise “tamam, sende AVIF çalışır, o zaman AVIF döneyim” diyebiliyor. Bu anlaşmanın adı içerik pazarlığı. Basit gibi görünür ama yanlış kurgulanırsa CDN, bu cevabı herkese tek tip zannedip yanlış varyantı saklayabiliyor.

İşte burada Vary: Accept devreye giriyor. Sunucu “ben bu cevabı Accept başlığına göre ürettim” dediğinde, aradaki cache’ler bunu ciddiye alıp anahtarı Accept’e göre farklılaştırıyor. Bu olmazsa, bir kez WebP gören cache, AVIF isteyen bir tarayıcıya da aynı WebP’yi itmeye çalışıyor. Sonra bazen görsel hiç açılmıyor, bazen açılıyor, işte o sabahki tuhaflıklar böyle doğuyor. Detayını merak ederseniz Accept başlığına dair kısa ve anlaşılır bir teknik özet için MDN’deki Accept başlığı anlatımı işinizi görür.

Buradaki pratik ölçü şu: Tarayıcının kabul ettiğine göre format seç, ancak bu seçimi CDN ve tarayıcı önbelleği anlayacak şekilde işaretle. Vary etiketi, doğru Content-Type ve doğru dosya eşleşmesi… Hepsi küçük ama kritik taşlar. Birini eksik bırakırsanız, “bende çalışıyor” cümlesi üretime çıktığınız an havada kalabiliyor.

Nginx ile Kırmadan Dönüşüm: Map, Try_Files ve Vary’ın Sessiz Uyumu

Nginx tarafında işin tadı map direktifiyle çıkıyor. Benim yıllardır kullandığım yaklaşım şu: Tarayıcı Accept’inde AVIF varsa AVIF, yoksa WebP, o da yoksa orijinal JPEG/PNG. Dosya adlandırmasını baştan planlamak gerekiyor. En rahat yöntem, aynı dosya gövdesini farklı uzantılarla kardeş yapmak. Mesela /img/hero.jpg için /img/hero.avif ve /img/hero.webp üretmek. Bu sayede tek bir URL’den, tarayıcıya göre doğru dosyayı döndürmek mümkün oluyor.

Nginx yapı taşı

# mime.types dosyanızda bu türler tanımlı değilse ekleyin:
# types {
#     image/avif avif;
#     image/webp webp;
# }

# 1) Accept'e göre hedef uzantıyı seç
map $http_accept $img_ext {
    ~*image/avif   ".avif";
    ~*image/webp   ".webp";
    default        "";
}

# 2) İstekteki URI'dan uzantıyı ayır
map $uri $no_ext {
    ~^(.+).(?:jpe?g|png)$ $1;
    default $uri;
}

server {
    # ...

    location ~* .(?:jpe?g|png)$ {
        add_header Vary Accept;
        # Önce dönüştürülmüş varsa onu, yoksa orijinali gönder
        try_files $no_ext$img_ext $uri =404;
    }
}

Burada üç küçük sihir var. Birincisi AVIF’i tercih ettik; çünkü dosyalar genelde daha ufak oluyor, ama AVIF yoksa WebP’ye düşüyor. İkincisi, Accept’e göre seçimi map ile üstte yaptık, bu performanslı ve okunaklı. Üçüncüsü, cache’lerin karışmaması için Vary: Accept ekledik. Bu üçlüyü kurunca, aynı URL’den herkes kendi formatına kavuşuyor.

Üretimde bir defa şuna takılmıştım: Bazı CDN’ler Accept’e göre otomatik vary’lamaz; yani cache anahtarına Accept’i siz dahil etmelisiniz. Aşağıda CDN kısmında bunu ayrıca konuşacağız. Bu Nginx kurgusunun güzelliği şu; dönüştürülmüş dosya yoksa orijinali dönüyor. Yani yarım kalmış bir dönüşüm pipeline’ı yüzünden görselin kaybolması zor.

Geliştirirken rahat test

Ben testleri genelde şu küçük komutlarla yapıyorum. Basit ama etkili:

# AVIF isteyen bir istemci gibi davran
curl -I -H "Accept: image/avif" https://site.com/img/hero.jpg

# WebP isteyen bir istemci
curl -I -H "Accept: image/webp" https://site.com/img/hero.jpg

# Hiçbir şey istemeyen (kendi haline bırak)
curl -I https://site.com/img/hero.jpg

Gelen Content-Type ve içerik uzunluğuna şöyle bir bakıyorum. Yanlış varyant dönerse hemen belli oluyor. Eğer üretimde dönüşümleri otomatikleştirmek istiyorsanız, görsellerin nasıl sıraya gireceğini, hangi kaliteyle dönüştürüleceğini ve cache’in nasıl temizleneceğini bir boru hattına bağlamak akıllıca. Bunun üstüne konuştuğum bir yazıyı da isterseniz bakabilirsiniz; AVIF/WebP ile bir görüntü optimizasyonu boru hattı kurma deneyimlerini orada toparlamıştım.

Apache/.htaccess ile Dönüşüm: Şefkatli Rewrite ve Vary

Apache tarafında da yol benzer. mod_rewrite ile istek geldiğinde önce dönüştürülmüş kardeşi arıyoruz, varsa ona yönlendiriyoruz. Yine Accept’e bakıyor ve bir Vary: Accept başlığı ekleyerek cache’lerin karışmasını engelliyoruz.

Apache yapı taşı

# mime türleri tanımlı değilse ekleyin
AddType image/avif avif
AddType image/webp webp

# cache'lere ipucu
Header merge Vary Accept

RewriteEngine On

# 1) AVIF varsa ve istemci AVIF kabul ediyorsa
RewriteCond %{REQUEST_FILENAME} (.+).(?:jpe?g|png)$ [NC]
RewriteCond %1.avif -f
RewriteCond %{HTTP_ACCEPT} "image/avif" [NC]
RewriteRule ^ %1.avif [L]

# 2) WebP varsa ve istemci WebP kabul ediyorsa
RewriteCond %{REQUEST_FILENAME} (.+).(?:jpe?g|png)$ [NC]
RewriteCond %1.webp -f
RewriteCond %{HTTP_ACCEPT} "image/webp" [NC]
RewriteRule ^ %1.webp [L]

# 3) Aksi halde orijinal dosyaya izin ver (varsayılan akış)

Burada özel bir nokta var: “Dosya var mı?” kontrolü. Böylece dönüştürme pipeline’ı yetişmediyse kullanıcıyı boşa bekletmiyoruz. Bir kez buna dikkat etmeyi unuttum; istekler 404’e düştü, izleme ekranında kırmızı bir dalga gördüm. Basit bir -f koşuluyla mesele bitti.

Apache’de mod_headers ile Vary’ı netleştirmek iyi bir alışkanlık. Ayrıca KeepAlive ve sıkıştırma ayarları zaten varsa, görsellerde ekstra bir şey yapmaya gerek yok. Asıl kazanç doğru format, doğru cache anahtarı ve kırılmayan URL mantığında.

CDN ile Barış: Cache-Key, Origin Shield ve Yanlış Varyantların Sessiz Talanı

CDN işin tatlı ama hassas tarafı. Lokasyon yakınlığı, hız ve trafik maliyeti derken herkesi mutlu ediyor. Fakat Accept’e göre farklı dosya döndürüyorsanız, CDN’in cache anahtarına Accept’i dahil etmesi şart. Aksi halde bir lokasyonda WebP olarak saklanan görsel, AVIF isteyenlere de aynen gidebiliyor. Sonra Safari’de boş kutu, Chrome’da pırıl pırıl görsel gibi tatsız sürprizler oluyor.

Genelde yapılması gereken iki şey var. Birincisi origin cevabına Vary: Accept eklemek. İkincisi CDN tarafında “cache key’e Accept’i ekle” demek. Bazı CDN’lerde bu, header bazlı vary ayarıyla çözülüyor. Bazılarında custom cache key kuralına Accept’i koyuyorsunuz. Nginx ve Apache örneklerinde Vary’ı eklemiştik; CDN bunu görürse çoğu zaman doğru anahtarı kurar, görmezse siz yine UI ya da kural setiyle dahil edersiniz.

Bir projede şunu yaşadım: CDN, Accept’in her farklı kombinasyonu için bambaşka cache anahtarı üretti ve cache parçalandı. Basit bir çözüm, sunucuda Accept’i “AVIF varsa AVIF, yoksa WebP, yoksa boş” gibi mantıksal bir map ile tek tipe indirip Vary’ı yine de eklemek. Böylece anahtar sayısı kontrol altında kalıyor. Nginx’teki map bu yüzden hoşuma gidiyor; Accept ne kadar kalabalık gelirse gelsin, bizim gözümüzde iki varyant var: AVIF veya WebP. Gerisi orijinal.

CDN tarafında Origin Shield kullanıyorsanız, ilk istekler origin’e daha az yük bindirir. Bir de akıllı invalidation yapmanızı öneririm. Yeni bir AVIF yayımladıysanız, ilgili URL’leri hedef alan küçük bir temizleme işlemi çok iş görüyor. Detaylı düşünmek isterseniz, bu konuyu bir tür hikaye tadında anlattığım yazıyı önerebilirim: Origin Shield ve akıllı cache anahtarıyla nefes alan bir CDN faturası.

Son bir not: Bazı CDN’ler Accept yerine Client-Hints başlıklarıyla (örneğin Sec-CH ailesi) vary ayarı yapmanızı da destekler. Bu yazıda Accept üzerinden gidiyoruz çünkü her yerde çalışıyor ve kafa karıştırmıyor. Eğer UI’de “Header’a göre vary” diye bir ayar görürseniz, gönül rahatlığıyla Accept’i işaretleyin.

SEO ile Dost: URL’leri Bozmadan Dönüştürmenin Zarif Yolları

Görsel performansı bir yana, SEO söz konusu olduğunda iki şeye dikkat ediyorum: URL istikrarı ve erişilebilirlik. URL’yi değiştirmeden, aynı yol üzerinden sadece formatı akıllıca seçmek hem paylaşımları hem de önbellekleri rahat ettiriyor. Bir de alt metinleri, boyut bilgisi ve lazy-load gibi günlük alışkanlıklar var; bunlar ayrı ayrı minik ama birleşince etkisi büyük.

Ben şu iki yaklaşımı seviyorum. Birincisi bu yazıdaki sunucu tarafı içerik pazarlığı: Aynı URL, farklı Content-Type. İkincisi, HTML’de <picture> elementiyle açıkça “önce AVIF, olmazsa WebP, o da olmazsa JPEG” demek. İkisini birlikte kullanmak da mümkün, ama çoğu projede biri yeterli oluyor. Picture yaklaşımı, front-end üzerinde daha görünür ve kontrol edilebilir.

Picture ile kibarca bildirmek

<picture>
  <source srcset="/images/hero.avif" type="image/avif">
  <source srcset="/images/hero.webp" type="image/webp">
  <img src="/images/hero.jpg" alt="Ürünün ön yüzünü gösteren kahraman görseli" width="1200" height="800" loading="lazy" decoding="async">
</picture>

Bu yöntemin güzelliği, tarayıcının kendi maharetini kullanması. Ayrıca width/height verdiğiniz için layout kayması azalıyor. Lazy-load ve decoding ipuçlarıyla da sayfa akışını rahatlatıyorsunuz. Görsellerin keşfi ve arama motoru tarafındaki ipuçları için şu rehber hoş bir özet sunar: image SEO için pratik öneriler.

Sunucu tarafı içerik pazarlığında ise Content-Type’ın doğru dönmesi ve Vary’ın net olması yeterli. URL değişmediğinden paylaşımlarla arası iyi olur, yönlendirme trafiği azaltılır. Kaynak haritası, site haritası gibi dosyalarda da ekstra bir şey yapmaya gerek kalmaz. Ben ek olarak, görselleri dönüştürürken orijinali saklı tutmayı seviyorum; beklenmedik bir tarayıcı davranışında geri adım atmak daha kolay.

Test, Saha Denemesi ve Log’lardan Küçük İpuçları

Teoride her şey güzel ama üretimde beklenmedik detaylar çıkar. Mesela bazı eski tarayıcılar Accept’i eksik gönderir ya da ara bir proxy bu başlığı budar. O yüzden ben “her koşulda orijinale düş” şeridini hep açık bırakıyorum. Yani dönüştürülmüş yoksa orijinal var. Bu yaklaşım yüzünden performanstan ödün vermiyorum; çünkü çoğunluk zaten AVIF/WebP alıyor, kalan azınlık ise sorunsuz görüntü görüyor.

Bir diğer pratik, log’lara kısa bir süre daha fazla bakmak. Accept başlıkları gözlemlendiğinde, hangi formatın ne kadar kullanıldığını görürsünüz. Eğer log tutmayı seviyorsanız, bunu bir süre panoda izlemek akıllıca oluyor. Ben bu iş için merkezi loglama kurulumuna güveniyorum; Loki + Promtail ile merkezi loglamayı kurduğum yazıda basit ve hafif bir yaklaşımı anlatmıştım. Görsel format kabul oranlarını bir grafiğe dökünce, dönüşüm stratejisini ayarlamak çok kolaylaşıyor.

Canlıya alırken sıfır kesinti prensibi de hayat kurtarıyor. Nginx/Apache konfigürasyonu bir kerede değil, küçük adımlarla açmak iyi fikir. Mesela önce sadece AVIF üretimi olan birkaç klasörde pazarlığı etkinleştirip izleyin, sonra tüm görsellere genişletin. Böylece yanlış bir rewrite hatası varsa etkisi sınırlı kalır. Bu bakış açısını bir başka yazıda, servisleri nasıl sakin sakin canlıya aldığımızı anlatırken de işlemiştim; merak ederseniz sıfır kesinti deploy üzerine notlar hoşunuza gidebilir.

Bir WordPress sitesinde bunu uyguladığımda, Docker ile kurduğum Nginx’in içinde sadece küçük bir map + try_files eklemek yetti. Görselleri dönüştürmeyi ayrı bir container’a verdiğim için web katmanı hiç yorulmadı. Benzer bir düzende ilerlemek isterseniz, WordPress’i VPS’te Docker ile yaşatma macerası yazısında basit bir çerçeve çizmiştim.

Küçük Tuzaklar: Yanlış Content-Type, 406’lar ve Karışan Cache’ler

En sık gördüğüm tuzaklardan biri, dosyayı AVIF olarak döndürüp Content-Type’ı image/jpeg bırakmak. Tarayıcı kafası karışınca görseli bazen hiç açmıyor, bazen de hatalı davranıyor. O yüzden mime türlerini Nginx/Apache tarafında tanımlı tuttuğunuzdan emin olun. Nginx’te bu mime.types dosyasına bakar; Apache’de AddType ile ekledik. Kısa ve net.

İkinci tuzak, Accept’e uymayan 406 Not Acceptable gibi yanıtlarla kullanıcıyı üzmek. Bence hiç gerek yok. Her zaman bir orijinal format emniyet kemeri kalsın. “AVIF yoksa WebP, o da yoksa JPEG” üçlüsü güvenli ve pratik. Sahnede hata yoksa kullanıcı da hata görmez.

Üçüncü tuzak, CDN cache anahtarında Accept’i unutmak. Bunu çözmek için hem Vary: Accept verin, hem de CDN tarafında explicit bir kural yazın. Bazı panellerde “Cache by header” ya da “Ignore/Include headers” şemaları vardır. Oraya Accept’i ekleyince iş biter. Nginx tarafındaki map ile Accept’i üç varyanta indirmeniz de cache’in parçalanmasını engeller. Nginx’in map davranışıyla ilgili orijinal dökümana göz atmak isteyenler için: ngx_http_map_module.

Dördüncü tuzak, dönüşümü anlık yapmak. İlk bakışta pratik görünür ama anlık AVIF üretimi yoğun trafikte CPU’yu yorabiliyor. Ben genelde bir sıra ve işleyiciyle, görselleri arka planda dönüştürüp hazır ettiriyorum. Böylece web katmanı sadece dosya seçiyor, dönüştürmüyor. Zaten bu sayede kırılma riski azalıyor, pageload süreleri daha istikrarlı kalıyor.

Kapanış: Zarif Bir Anlaşma, Daha Hızlı Bir Dünya

WebP/AVIF’i kırmadan sunmak, aslında küçük kararların uyumuyla mümkün oluyor. Tarayıcıyla içerik pazarlığı yap, Nginx/Apache’de basit ve güvenli bir rewrite kur, CDN cache anahtarına Accept’i ekle ve SEO tarafında URL istikrarını koru. Hepsi bir arada olunca, aynı URL’den herkes kendi formatını alıyor ve performans ivmesi hissedilir hale geliyor.

Pratik bir yol haritası şöyle özetlenebilir: Önce dönüştürme hattını kurup görselleri AVIF/WebP olarak hazırlayın. Nginx/Apache’de map + try_files ya da RewriteCond + -f ile kırılmayan bir geçiş oluşturun. Vary: Accept eklemeyi unutmayın ve CDN’de cache anahtarını Accept’e göre ayarlayın. HTML tarafında isterseniz <picture> kullanarak ekstra kontrol sağlayın. Canlıda küçük bir trafik yüzdesiyle deneyip, log’larda kabul oranlarını takip edin. Birkaç gün sabırla izlediğinizde zaten doğru ayarlar kendini belli ediyor.

Umarım bu yolculuk size fikir vermiştir. Eğer kafanıza takılan noktalar olursa not alın, bir sonraki bakım penceresinde ufak ufak deneyin. Bu konuların çoğu küçük dokunuşlarla rayına oturuyor. Benzer tempoda, TLS ayarlarından Nginx disiplinine kadar pek çok başlık üzerine konuştuğumuz yazıları da seviyorsanız, arada bir uğrayın. Şimdilik benden bu kadar; umarım sitenizdeki görseller, kimseyi kırmadan en hızlı haliyle parıldar. Bir dahaki yazıda görüşmek üzere.

Sıkça Sorulan Sorular

Tarayıcının Accept başlığına göre seçim yap. Nginx’te map + try_files, Apache’de RewriteCond ile önce AVIF, sonra WebP’yi dene; yoksa orijinali dön. Vary: Accept eklemeyi unutma.

Origin cevabına Vary: Accept koy ve CDN’de cache anahtarına Accept’i dahil et. Böylece AVIF isteyenle WebP isteyenin varyantları karışmaz.

Evet. Aynı URL’den tarayıcıya göre farklı Content-Type dönebilirsin. İstersen HTML’de picture kullanarak AVIF/WebP önceliği verip JPEG’i güvenli yedek bırak.