GEON GEON
GEO Rehber 19 saat önce 6 dk

Sitemiz AI Motorlarına Görünmezdi: Bir Prerender/Deploy Hatasının Anatomisi

GEON, AI arama görünürlüğü satan bir şirket olarak kendi sitesini AI tarayıcılarının okuyamayacağı bir halde yayınladı. Hata hiçbir yerde hata gibi görünmedi: her istek HTTP 200 döndü. Ölçtüğümüz veriler, kök nedeni ve atlanamayan bir deploy gate'i kurmak için yazdığımız script.

Sitemiz AI Motorlarına Görünmezdi: Bir Prerender/Deploy Hatasının Anatomisi

Ben Deniz, GEON'da çalışıyorum — işimiz markaların ChatGPT, Perplexity, Claude, Gemini, Grok ve DeepSeek'te ne kadar görünür olduğunu ölçmek. Ve kendi sitemiz, AI tarayıcılarının gözünde neredeyse boş bir sayfaydı. Ne bir hata mesajı vardı, ne bir uyarı, ne kırık bir link. Her istek HTTP 200 döndürüyordu. Sadece içerik yoktu. Bu yazı o hatanın ne olduğu, neden fark edilmeden durduğu ve düzeltmenin neden "daha iyi bir build script'i" değil, atlanması imkânsız bir kontrol noktası olduğu üzerine.

Ölçtüğümüz şey

4 Ağustos 2026'da, https://usegeon.com'a User-Agent: GPTBot/1.0 başlığıyla istek attık — yani tam olarak OpenAI'nin tarayıcısının göreceği yanıtı aldık. Sonuç:

Rota Sonuç
/ HTTP 200, text/html, 3.359 byte, <h1> yok, 6 görünür kelime
/blog Birebir aynı 3.359 byte'lık yanıt
/docs Birebir aynı 3.359 byte'lık yanıt
/sitemap.xml HTTP 200 ama text/html — robots.txt'nin işaret ettiği sitemap, XML değil bir web sayfası döndürüyordu

6 kelime dediğim gerçekten 6 kelime: JavaScript çalıştırmayan bir istemci için sayfanın tamamı GEON - AI Platform Visibility Monitoring başlığından ibaretti. <div id="root"> boştu, çünkü içeriği dolduran React kodu hiç çalışmamıştı. GPTBot, ClaudeBot ve PerplexityBot'un JavaScript çalıştırdığına dair güvenebileceğimiz bir sağlayıcı taahhüdü yok — ve render etmeyen her istemci için orada bir ürün sayfası, bir blog, bir dokümantasyon yoktu; sadece o başlık vardı.

Üstelik <head> etiketinin içi de tamamen doğruydu: gerçek bir <title>, doğru bir meta description, eksiksiz Open Graph ve Twitter Card etiketleri, canonical link, favicon ve manifest referansları — hepsi yerli yerindeydi. Sayfa kaynağına hızlıca göz atan biri her şeyin normal olduğunu düşünebilirdi. Sorun <body>'deydi: React'in doldurması gereken <div id="root"> hiç dolmamıştı.

Buna karşılık robots.txt doğruydu ve https://usegeon.com/sitemap.xml'i işaret ediyordu; llms.txt da doğruydu, 6.549 byte'lık gerçek içeriğiyle sorunsuz duruyordu. Yani tarayıcıya dönük yüzeyin yarısı doğruydu — ve fark edilmeden durmasının sebebi tam olarak buydu. robots.txt'yi kontrol ettiğinizde her şey normal görünür. llms.txt'yi kontrol ettiğinizde her şey normal görünür. <head>'e bakarsanız her şey normal görünür. Asıl gövde içeriğine bakmadıkça hatayı göremezsiniz.

Neden oldu

Sebep basit ve can sıkıcı: prerender zinciri kök dizindeki yarn build komutunun içinde yaşıyor — statik HTML üreten script'ler, sitemap.xml üreten script ve blog/docs sayfalarını önceden render eden adımlar hep o zincire bağlı. Ama firebase deploy --only hosting ya da web klasöründe düz bir yarn build çalıştırmak bu zinciri hiç tetiklemiyor — sadece vite build'i çalıştırıyor, o da tek başına boş bir SPA kabuğu üretiyor: içi React'in doldurmasını bekleyen bir <div id="root">. dist klasöründe ne varsa o yayınlanıyor. Zincirin doğru versiyonunu kullanmayı zorunlu kılan hiçbir mekanizma yoktu.

Bu, kırık bir kod değildi. Doğru script, yanlış yerden çağrılmıştı — ya da hiç çağrılmamıştı. Build başarıyla bitiyordu, deploy başarıyla bitiyordu, hiçbir CI adımı kırmızıya dönmüyordu. Sonuç sadece "eksik" idi, "bozuk" değil — ve bu iki hata sınıfı çok farklı şekillerde yakalanır.

Düzeltme: build script'i değil, geçilemez bir kapı

Doğru script'i hatırlatmak yetmezdi — kimse unutmuyordu, sadece farklı bir komut çalıştırıyordu. Bu yüzden web/scripts/verify-prerender.ts adında salt-okunur bir doğrulama script'i yazdık ve firebase.json içindeki hosting.predeploy adımına bağladık. Bunun anlamı: deploy nasıl tetiklenirse tetiklensin — firebase deploy, CI, elle çalıştırılan komut — bu script önce çalışıyor ve geçmeden hosting'e hiçbir şey yüklenmiyor.

Script şunu yapıyor: /, /docs, /blog ve bir Türkçe karşılaştırma sayfası (/karsilastirma/peec-ai — İngilizce sayfalar üretilmeye devam ederken Türkçe prerender'ın sessizce bozulmasını yakalamak için eklenmiş bir kanarya) için üretilen statik HTML dosyalarını açıyor, her birinde gerçek bir <h1> etiketi olduğunu ve script/style etiketleri temizlendikten sonra en az 20 görünür kelime kaldığını doğruluyor — yani JavaScript'siz bir tarayıcının gerçekten göreceği şeyi ölçüyor. Ayrıca sitemap.xml'in <urlset> veya <sitemapindex> içeren gerçek XML olduğunu, SPA'nın fallback HTML'i olmadığını kontrol ediyor. Herhangi biri eksikse ya da eşiğin altındaysa script hangi sayfanın eksik olduğunu isim isim yazıp 1 koduyla çıkıyor ve deploy'u durduruyor. Dosya sistemine hiçbir şey yazmıyor, sadece okuyor, ve çalışması yaklaşık 0.2 saniye sürüyor.

Doğru bir build'de script şunu raporluyor:

✓ Pre-deploy verification passed
  /: 47 words
  /docs: 46 words
  /blog: 1117 words
  /karsilastirma/peec-ai: 641 words
  sitemap.xml: 94 URLs

Zinciri atlayan bir build'de ise şunu:

✖ Pre-deploy verification FAILED — refusing to publish an unprerendered build.
  • / — no <h1> in static HTML (SPA shell was deployed)
  • / — only 6 visible words without JS (min 20)
  • /docs — dist/docs/index.html is missing
  • sitemap.xml exists but is not valid XML (likely the SPA fallback)

Prodüksiyonda hâlâ ne durumda

Bu düzeltme kod tabanına merge edildi. Ama bu yazıyı yazdığım anda prodüksiyonu tekrar kontrol ettim ve yeniden deploy henüz yapılmamıştı: https://usegeon.com/ GPTBot User-Agent'ıyla hâlâ aynı 3.359 byte'lık, <h1>'siz, 6 kelimelik yanıtı döndürüyor; /sitemap.xml hâlâ text/html dönüyor. Kapı kodda duruyor ama henüz kapatılmadı. Bunu burada yazmamın sebebi basit: prodüksiyonu düzelttiğimizi iddia edip sonra düzelmediğini fark etmek, tam olarak bu yazının konusu olan hatayı bir kez daha yapmak olurdu.

Genel ders "SPA'lar kötüdür" değil

Buradaki asıl ders framework seçimiyle ilgili değil. Doğru olan build zinciri zaten yazılmıştı — sorun onun var olmaması değil, onu kullanmayı zorunlu kılan hiçbir noktanın olmamasıydı. Uygulanmayan bir kural, er ya da geç atlanacak bir kuraldır; bu insan hatası değil, olasılık meselesidir. İkinci ve daha önemli kısım: bu hata sınıfı sessizdir. Hata kodu yok, alarm yok, kırık build yok — sadece yokluk var. HTTP 200 her şeyin yolunda olduğunu söyler; içeriğin oradan gittiğini söylemez.

Kendi sitenizi nasıl kontrol edersiniz

İki komut, bir dakikanızı alır:

curl -s -o /dev/null -w "%{http_code} %{size_download}\n" \
  -A "GPTBot/1.0" https://siteniz.com/

curl -s -A "GPTBot/1.0" https://siteniz.com/sitemap.xml | head -c 200

Birinci komut ana sayfanızın boyutunu verir — birden fazla rotanız birebir aynı byte sayısını döndürüyorsa (ana sayfa, blog, dokümantasyon hepsi aynı boyutta) büyük olasılıkla hepsi aynı boş kabuğu servis ediyordur. İkinci komut sitemap'inizin gerçekten XML olup olmadığını gösterir; çıktı <!doctype html> ile başlıyorsa sitemap'iniz de kabuk döndürüyor demektir. Daha köklü bir kontrol için: curl -A "GPTBot/1.0" https://siteniz.com/ | grep -c "<h1" — sıfır dönerse JavaScript'siz bir tarayıcı sayfanızda başlık göremiyor demektir.

Ne yapmalı

Statik HTML üreten bir build adımınız varsa, onu atlayan hiçbir deploy yolunun kalmadığından emin olun — bizim yaptığımız gibi hosting.predeploy'a (ya da eşdeğerine) bağlayın, ekip üyelerine hatırlatmaya güvenmeyin. Her deploy sonrası GPTBot User-Agent'ıyla en az ana sayfanızı ve sitemap'inizi elle kontrol edin; bunu otomatikleştirmek daha iyidir ama elle kontrol sıfır maliyetlidir. Ve HTTP 200'ün "çalışıyor" anlamına gelmediğini kabul edin — içerik orada mı, o ayrı bir soru.

Deniz

Deniz

İçerik & GEO Stratejisi

İlgili Yazılar

GEO Rehber

llms.txt Gerçekten Okunuyor mu? Kendi Loglarınızda Nasıl Ölçersiniz

7 dk
GEO Rehber

robots.txt 2026: Tek Bir Disallow Satırı Yetmiyor

7 dk
GEO Rehber

AI Arama Motorlarında Kapak Görseli Stratejisi

6 dk