codebiy.com'un sayfaları önceden üretiliyor: Next.js 16 ve React 19, her sayfayı bir kez, build sırasında üretiyor. Şablonlarda iki reklam alanı var: blog listesinde üçüncü yazıdan sonra ve her yazının altında. Bundan üç gereklilik çıkıyor: reklam kodu her ziyaretçi için HTML'de bulunmalı, ziyaretçi karar vermeden reklam ve analitik kodu hiçbir şey saklamamalı ve içerik güvenliği politikası (CSP) kimin çalışabileceğini tek tek saymalı.
Kontrol ettiğimiz kadarı şu: 9 Ekim 2026'da canlı sitede, izin verildikten sonra reklam kütüphanesinin istediği her şey, tek bir frame dışında, politikadan geçti. Onu da sonradan politikaya ekledik. O ziyarette reklam gösterilmedi, çünkü canlıdaki build'de henüz reklam alanı ID'si yok. Yani izin akışını ve kütüphanenin kendi isteklerini kontrol ettik, gösterilen bir reklamı ise henüz kontrol etmedik.
Reklam kütüphanesi her ziyaretçi için yükleniyor, izin varsayılan olarak reddedilmiş
Ağustos 2026'daki yeniden yazımdan önce AdSense yükleyicisi çerez bildiriminin arkasında bekliyor, izin gelince de dolduracak hiçbir şeyi olmayan bir kütüphaneyi yüklüyordu: repo'da tek bir reklam alanı yoktu. Yeniden yazımda gerçek reklam alanları ekledik. İlk sürümde yükleyici yine bildirimin arkasındaydı. Bunu aynı öğleden sonra, iki sebeple değiştirdik.
Yükleyici bildirimin arkasındayken sunucudan gelen HTML'de reklam kodu olmuyor. Google'ın reklam göstermeye hazır olmayan siteler için yardım sayfasında, sebepler arasında reklam kodunun eksik ya da tamamlanmamış olması (“Your ad code is missing or incomplete”) sayılıyor ve kodun sitenin HTML'inde olup olmadığı soruluyor. Sayfayı çekip hiçbir düğmeye basmayan herkes için, ister incelemeyi yapan olsun ister crawler, bizim sayfamızda kod yoktu. Site incelemesinin sayfayı böyle gördüğü bizim çıkarımımız, Google'ın söylediği bir şey değil.
İkinci sebep Consent Mode'un çalışma biçimi: sayfa varsayılanları belirler, zaten yüklenmiş olan etiket onları okur ve ona göre davranır (Google'ın kurulum rehberi). Hiç yüklenmeyen bir etiketin, Google'ın okuyabileceği bir izin durumu da olmaz.
Bu yüzden yükleyiciyi <head> içinde render edilen bir server component'e çevirdik. Sabit bir sırayla iki <script defer> etiketi üretiyor: önce kendi varsayılanlar dosyamız, sonra Google'ın kütüphanesi. Varsayılanlar Consent Mode v2'ye göre yazılı. Bu sürüm, eski iki sinyale ad_user_data ile ad_personalization sinyallerini ekledi. Bütün sinyaller denied olarak başlıyor:
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("consent", "default", {
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
analytics_storage: "denied",
wait_for_update: 500,
});
Google'ın rehberine göre varsayılanlar, veri gönderen her komuttan önce çalışmalı. Alışılmış yöntem, async bir script etiketinin yanına inline script koymaktır, ama React 19'da bu sıra bozuluyor. React, <script async src> etiketini <head> içine taşır, inline script'i ise render edildiği yerde bırakır. İlk denememizde kütüphane varsayılanların önüne geçti. O yüzden varsayılanlar public/ altında bir dosyada duruyor ve iki etiket de defer. defer ile yüklenen script'ler belge sırasıyla çalışır, yani sırayı tarayıcı garanti eder.
Analytics yerinden oynamadı: script'i hâlâ yalnızca ziyaretçi analitiğe izin verdikten sonra render ediliyor.
Karardan önce reklam çerezi yok: pauseAdRequests
Varsayılanları denied yapmakla iş bitmedi. 8 Ağustos 2026'daki testimizde, bütün sinyaller reddedilmişken, hiçbir karar vermemiş bir tarayıcıya yine de doubleclick.net'ten test_cookie geldi: kütüphaneyi yüklemek yetiyordu. Bu, izin alınmadan yazılmış, zorunlu olmayan bir çerez.
Varsayılanlar dosyasına iki satır daha girdi:
window.adsbygoogle = window.adsbygoogle || [];
window.adsbygoogle.pauseAdRequests = 1;
pauseAdRequests, yeniden 0 yapılana kadar bütün reklam isteklerini bekletir. Bu değeri, izin güncellemesiyle birlikte tek bir React effect'inde 0 yapıyoruz:
const value = (ok: boolean) => (ok ? "granted" : "denied");
window.gtag?.("consent", "update", {
ad_storage: value(consent.ads),
ad_user_data: value(consent.ads),
ad_personalization: value(consent.ads),
analytics_storage: value(consent.analytics),
});
if (window.adsbygoogle && consent.ads)
window.adsbygoogle.pauseAdRequests = 0;
Effect, karar her değiştiğinde çalışıyor. Yeniden gelen ziyaretçi bildirimi görmüyor: saklanan karar, React sayfayı tarayıcıda devraldıktan (hydration) sonra localStorage'dan okunuyor ve aynı effect onu her sayfa yüklemesinde yeniden gönderiyor. wait_for_update: 500 bu boşluk için var. Google'ın etiketlerinden, veri göndermeden önce bir güncelleme için 500 ms'ye kadar beklemelerini istiyor. Yavaş bir telefonda hydration'ın bu süreye ne sıklıkla sığdığını ölçmedik. Sığmadığında etiketler varsayılanlarla, yani izni reddedilmiş sayarak devam ediyor, reklam istekleri ise iki durumda da bekliyor.
Güncelleme gtag() üzerinden gidiyor. dataLayer'a düz bir dizi push etmek aynı şeymiş gibi görünür ama değildir. İkisini de 9 Ekim 2026'da masaüstü Chrome'da, bir test sayfasında denedik: dataLayer.push(["consent", "update", …]) çağrısından sonra gtag.js'in izin durumunda güncelleme yoktu, konsol da boştu. Aynı değerler gtag() ile gönderilince güncelleme işlendi.
Kararla ilgili diğer ayrıntılar:
- Reddedilince ne reklam kalıyor ne boşluk. Reklama izin verilmediyse reklam alanı bileşeni hiçbir şey render etmiyor, kutusunu bile.
- Kararın süresi doluyor. Zaman damgasıyla saklanıyor ve bir yıl geçerli.
- Geri dönüş yolu var. Footer'daki “Çerez ayarları” satırı bildirimi yeniden ekrana getiriyor.
Buna bir de statik sayfalara özgü bir hata eklendi. Önceden üretilmiş sayfa herkes için aynı HTML'dir, o anki ziyaretçinin daha önce seçim yapıp yapmadığını bilemez. İlk sürümümüz “henüz okunmadı” ile “karar verilmedi” durumlarını tek durum sayıyordu. Bu yüzden bildirim, aylar önce karar vermiş ziyaretçiler de dâhil herkeste, her yenilemede görünüp kayboluyordu. Çözüm, localStorage okunduktan sonra true olan bir ready flag'i. O ana kadar bildirim hiçbir şey render etmiyor.
Sitenin gönderdiği CSP: her origin adıyla yazılı, nonce yok
Politikayı bütün route'lar için next.config.ts içinde belirliyoruz. Sitenin gönderdiği header şu, okunabilsin diye satırlara böldük:
default-src 'self';
script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com
https://www.googletagmanager.com https://googleads.g.doubleclick.net
https://tpc.googlesyndication.com https://adservice.google.com
https://ep1.adtrafficquality.google https://ep2.adtrafficquality.google
https://challenges.cloudflare.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://pagead2.googlesyndication.com
https://www.google-analytics.com https://googleads.g.doubleclick.net
https://tpc.googlesyndication.com https://ep1.adtrafficquality.google
https://ep2.adtrafficquality.google;
font-src 'self' data:;
connect-src 'self' https://www.google-analytics.com
https://pagead2.googlesyndication.com https://googleads.g.doubleclick.net
https://ep1.adtrafficquality.google https://ep2.adtrafficquality.google;
frame-src https://googleads.g.doubleclick.net
https://tpc.googlesyndication.com https://ep1.adtrafficquality.google
https://ep2.adtrafficquality.google https://www.google.com
https://challenges.cloudflare.com;
object-src 'none'; base-uri 'self'; form-action 'none';
frame-ancestors 'none'; upgrade-insecure-requests
Google'ın reklam ve analitik origin'leri tam alan adlarıyla yazılı. *.google.com da, tek başına https: de yok. Listede bunların dışında tek bir üçüncü taraf var: iletişim formundaki doğrulama için kullandığımız Cloudflare Turnstile.
Her origin adıyla yazılı, bütün izin sinyalleri de varsayılan olarak reddedilmiş.
İzin koşulu bir CSP ihlalini gizliyordu. İki adtrafficquality.google origin'i ilk politikada yoktu. Biri Sodar adlı bir script sunuyor, ikisinin ne işe yaradığı da alan adından anlaşılıyor: Ad Traffic Quality, Google'ın geçersiz trafiğe karşı yürüttüğü çalışmanın adı. Bunlar ancak kütüphane herkes için yüklenmeye başlayınca ortaya çıktı. Yükleyici bildirimin arkasındayken, kabul edilmemiş bir oturumda kütüphane hiç çalışmıyordu, yani o oturumda hiçbir şey engellenmiyordu. Politika ise kabul eden her ziyaretçi için yine yanlıştı.
Nonce yok ve script-src içinde 'unsafe-inline' var. Politikanın zayıf noktası bu. Nonce her yanıtta farklı olmak zorunda ve Next.js onu yalnızca istek anında render ettiği sayfalara ekleyebiliyor. Statik sayfalardan vazgeçmedik ve alan adlarına dayalı bir izin listesiyle (allowlist) yetindik. Bu, strict CSP değil: sayfaya enjekte edilmiş bir inline script yine çalışır. Politikanın sınırladığı şeyler şunlar: script'lerin nereden yüklenebileceği, verinin nereye gönderilebileceği, frame içine neyin alınabileceği ve bir formun herhangi bir yere gönderilip gönderilemeyeceği. Statik sayfalarda alışılmış çözüm hash kullanmak. Next.js, bunun için sunduğu desteği (Subresource Integrity) deneysel olarak işaretliyor, biz de denemedik.
Reklam alanı: ayrılmış yükseklik, ortam değişkeninden gelen ID
Yüksekliğini önceden ayırıyor. Wrapper'ın minimum yüksekliği var, varsayılan olarak 280 piksel. Geç gelen reklam, altındaki metni itmiyor. Wrapper'ın kendisi ise ancak izin veren bir karar okunduktan sonra render ediliyor. Yeniden gelen ziyaretçinin sayfasında, wrapper'ın altında ne varsa o anda aşağı kayıyor. Bu layout shift'i (CLS) ölçmedik.
ID'sini ortam değişkeninden alıyor. Reklam alanı ID'si sır değil: reklamı gösteren her sayfada yazıyor. Onu kodun dışında tutmamızın sebebi başka. ID tanımlı değilse bileşen hiçbir şey render etmiyor. Uydurma bir ID ise yapılandırılmış gibi görünür ve hiçbir şey göstermez. Bunlar NEXT_PUBLIC_ değerleri, yani image build edilirken tanımlı olmaları gerekiyor. Ayrıntısı Docker build'imizi anlattığımız yazıda.
Kontrollerin ve canlı ziyaretin gösterdikleri, göstermedikleri
Bir Playwright script'i, yerel build üzerinde şu kontrolleri çalıştırıyor:
- Politika. Ana sayfayı iki dilde, bildirime dokunmadan yüklüyor. Kendi CSP'miz bir isteği engellerse başarısız oluyor.
- İzin. İngilizce blog listesinde, karardan önce bir izin varsayılanı bulunmalı ve bütün sinyalleri
deniedolmalı. “Accept all” (Türkçe sayfada “Tümünü kabul et”) düğmesine basıldıktan sonraad_personalizationdeğerigrantedolan bir güncelleme gönderilmiş, saklanan karar da zaman damgası almış olmalı. - Çerez. Blog listesini temiz bir tarayıcı oturumunda açıyor ve iki buçuk saniye bekliyor. Tarayıcıda başka bir alan adından çerez varsa başarısız oluyor.
- Layout shift. Ana sayfada kümülatif layout shift 0,01'i aşarsa başarısız oluyor.
Bunlar karardan önceki durumu ve kabul edince güncellemenin gittiğini kanıtlıyor. Bu kontrollerde dolu bir reklam hiç render edilmiyor, çünkü yerelde reklam alanı ID'leri tanımsız. Politika kontrolü kabulden sonra hiç çalışmıyor, layout shift kontrolü de reklam alanı olmayan bir sayfada çalışıyor. Bu tür boşlukları yapay zekâ agent'larının yazdığı front-end işini kontrol etmek üzerine yazımızda anlattık.
9 Ekim 2026'da canlı siteyi bir kez script'le ziyaret ettik: masaüstü Chrome, İngilizce bir yazı sayfası, “Accept all”. Karardan önce tarayıcıda hiçbir çerez yoktu. O güne kadar başka bir ziyaretçiye karardan önce bir çerez gelebiliyordu, o da sitenin kendi çereziydi: sayfanın dili tarayıcının dilinden farklıysa next-intl middleware'i ilk yanıtta NEXT_LOCALE adlı bir oturum çerezi yazıyordu. Bunu ne çerez kontrolümüzde ne de bu ziyarette görebilirdik: ikisinde de İngilizce bir tarayıcıyla İngilizce bir sayfa açılıyordu, kontrol de yalnızca başka alan adlarından gelen çerezlere bakıyor. Çerezi kapattık (localeCookie: false): sitedeki her bağlantıda dil kodu olduğu için çerez yalnızca dil kodu olmayan bir adreste işe yarıyordu.
Karardan sonra reklam kütüphanesi pagead2.googlesyndication.com, googleads.g.doubleclick.net ve iki adtrafficquality.google alan adına, analitik ise www.googletagmanager.com ile www.google-analytics.com alan adlarına istek gönderdi. Reklam kütüphanesinin bir isteği dışında bunların hepsi politikadan geçti: https://www.google.com/recaptcha/api2/aframe adresine açılan frame'i frame-src engelledi. O origin, yukarıdaki header'da artık var. Karardan sonra tarayıcıda üç çerez vardı: analitiğin _ga ve _ga_<id> çerezleri ile doubleclick.net üzerindeki test_cookie.
Politika, bizim eklemediğimiz bir script'i de engelliyor: Cloudflare'in static.cloudflareinsights.com adresinden gelen analitik beacon'ı, açtığımız her canlı sayfada karardan önce de sonra da engellendi. Yerel kontrol bunu göremiyor, çünkü beacon yerel build'de yok.
Hiçbir reklam alanı dolmadı. Kütüphanenin kendi yer tutucusu unfilled bildirdi. Yazının reklam alanı da render edilmedi, çünkü canlıdaki build'de henüz reklam alanı ID'si tanımlı değil. Politikayı kütüphanenin istekleriyle kontrol ettik, gösterilen bir reklamla henüz değil. Bu yüzden liste daha da uzayabilir.
Bunların hiçbiri hukuki bir değerlendirme değil. Kontroller davranışı tarif ediyor: ne yükleniyor, ne saklanıyor ve ne zaman. Çerez bildirimimiz, Google'ın sertifikalandırdığı bir izin yönetimi platformu (CMP) da değil. Google, Avrupa Ekonomik Alanı (AEA), Birleşik Krallık ve İsviçre'deki ziyaretçilere kişiselleştirilmiş reklam göstermek için IAB'nin Transparency and Consent Framework'üyle entegre, sertifikalı bir CMP şart koşuyor. O sayfaya göre kişiselleştirilmiş reklam yalnızca sertifikalı bir CMP'den gelen trafiğe gösterilebiliyor. Sertifikasız bir CMP'den gelen trafik ise kişiselleştirilmemiş ya da sınırlı reklamlara uygun sayılabiliyor.
Statik sitede AdSense için kontrol listesi
- Reklam kütüphanesini herkes için HTML'e koyun ve kararı Consent Mode ile bildirin. AEA, Birleşik Krallık ve İsviçre'de kişiselleştirilmiş reklam için karar, Google sertifikalı bir CMP'den gelmek zorunda.
- Varsayılanları bir dosyada tutun ve iki script'i de
deferile yükleyin, önce varsayılanlar gelsin. - Temiz bir tarayıcı profilinde, hiçbir şeye tıklamadan çerezlere bakın. Bir kez de dili sayfanınkinden farklı bir tarayıcıyla bakın. Reklam ağından gelen bir çerez varsa aradığınız ayar
pauseAdRequests. Çerez sizin sitenizinse i18n middleware'inize bakın. - İzin güncellemelerini düz bir dizi olarak değil,
gtag()üzerinden gönderin. Saklanan kararı her sayfa yüklemesinde yeniden gönderin. - Origin'leri tek tek yazın ve politika kontrolünü canlı sitede, kabul ettikten sonra da çalıştırın. Listenin uzayacağını hesaba katın.
- Reklam alanının yüksekliğini önceden ayırın. ID tanımlı değilse bileşen hiçbir şey render etmesin.