SPA SEO Rehberi
Tek sayfalı uygulamaları (React Router, Vue Router, Angular Router) taranabilir ve dizine eklenebilir kılmanın yollarını açıklar: app-shell ve soft-404 sorunları, History API ile hash yönlendirmesi arasındaki fark, SSR/prerendering ile rota başına HTML, rota başına canonical ve title değerleri ve istemci tarafı rotalar için sitemap üretimi.
Diller
Tek sayfalı bir uygulama, sunucudan yeni bir sayfa istemek yerine tek bir belge yükleyen ve görünümleri JavaScript ile değiştiren uygulamadır. Birçok SPA bunu “app shell” ile uygular — sunucu her rota için gerçek HTML yerine neredeyse boş bir kabuk döndürür — ancak bu her SPA için geçerli bir kural değildir; varsaymadan önce her rotaya doğrudan yapılan isteğin ne döndürdüğünü kontrol edin. Çıplak app-shell kurulumunun sorun olduğu yerde çözümün iki bağımsız yarısı vardır: adreslenebilirlik (hash/#! parçaları yerine History API ile her görünüm için gerçek bir URL) ve içerik kullanılabilirliği (SSR, prerendering/SSG veya meta-framework ile bu URL’lerin istek üzerine benzersiz HTML döndürmesi). Ayrıca her rotanın title, canonical ve robots durumunu doğrudan girişte ve oluşturma sonrasında doğrulayın; soft-not-found durumlarını ele alın (yönlendiriciler “not found” görünümleri için 200 durumunu koruma eğilimindedir — kendisi not-found döndüren bir URL’ye yönlendirin veya oluşturulmuş noindex ekleyin) ve sitemap’teki her rotanın gerçek, dizine eklenebilir HTML’ye çözüldüğünü doğrulayın.
TL;DR — Tek sayfalı bir uygulama (SPA), sunucudan bir sayfa yükler ve ardından tam yenileme olmadan “sayfalar” arasında JavaScript ile geçiş yapar. Sorun şu: birçok SPA, arama motoru hangi URL’yi isterse istesin aynı neredeyse boş başlangıç sayfasını gönderir — bu, SPA’ların yaygın kurulma biçiminin ortak bir riskidir; her SPA için geçerli bir kural değildir. Çözüm için her rotanın gerçek bir URL’si ve gerçek HTML’i olduğundan emin olun — genellikle sayfaları sunucuda oluşturarak veya önceden derleyerek.
SPA nedir
Tek sayfalı bir uygulama, tam belge gezinmesi olmadan istemci tarafındaki görünümleri ve rotaları güncelleyen bir web sitesidir. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Google JavaScript SPA’larını oluşturabilir; ancak taranabilir URL’ler, bağlantılar, durum yönetimi ve oluşturulmuş içerik yine de gereklidir. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Tek sayfalı uygulama, tek bir HTML sayfası olarak oluşturulmuş bir web sitesidir. Ürünler sayfasından hakkında sayfasına geçerken JavaScript, sunucudan tamamen yeni bir sayfa istemek yerine ekranda görünen içeriği değiştirir. React (React Router ile), Vue (Vue Router ile) ve Angular varsayılan olarak bu şekilde çalışır. Hızlı ve uygulama benzeri hissettirdiği için popülerdir.
Risk, sunucunun ne gönderdiğidir. Birçok SPA “app shell” uygulamasını kullanır: biri (Google dahil) uygulamayı ilk kez yüklediğinde sunucu çoğunlukla boş bir kabuk döndürür ve gerçek içerik daha sonra tarayıcıda oluşturulur. Bu, SPA oluşturmanın yaygın bir yoludur — SPA’nın tanımı değildir ve her SPA böyle çalışmaz. Ancak site gerçekten çıplak bir app shell gönderiyorsa arıza biçimi gerçektir: Google ayrı ayrı /about ve /products isterse, sunucunun gönderdiği şeyde rota fark yaratmadığı için ikisi için de aynı boş kabuğu alabilir. Uygulamanızın bu durumda olup olmadığını anlamanın yolu, SPA olmasından varsayım çıkarmak değil, her rotaya doğrudan yapılan isteğin gerçekten ne döndürdüğünü kontrol etmektir.
Bu SEO’ya neden zarar verir
Arama motorlarının sıralama yapabilmesi için içeriğinizi görmesi gerekir. Çıplak bir SPA’de üç şey genellikle ters gider:
- Her URL sunucuya aynı görünür. Deep link’ler, paylaşımlar ve tarayıcılar aynı kabuğa ulaşır.
- 404 sayfaları hâlâ “200 OK” der. JavaScript yönlendiricisi sayfa teknik olarak başarı bildirse de “bulunamadı” ekranı gösterebilir; Google da boş sayfaları dizine ekleyebilir.
- Yanlış URL’ler. Eski SPA’lar
example.com/#/productsgibi adresler kullanıyordu. Google bunları güvenilir biçimde dizine ekleyemez.
Nasıl düzeltilir (kısa sürüm)
- Her görünüme gerçek bir URL verin.
/productstabanlı yollar yerine tarayıcının History API’sini (#gibi temiz yollar) kullanın. - Her URL için gerçek HTML gönderin. Sayfaları sunucuda oluşturun (SSR) veya önceden oluşturun (prerendering). Sıfırdan başlıyorsanız bunu sizin için yapan Next.js, Nuxt, SvelteKit veya Angular SSR gibi bir framework işinizi kolaylaştırır.
- Her sayfaya kendi başlığını ve açıklamasını verin; rota değiştiğinde bunlar da değişsin.
- Bağlantıları gerçek bağlantı yapın (
<a href>), tıklanabilir<div>değil.
Mekaniği — hash yönlendirmesinin neden başarısız olduğunu, canonical’larda “en kısıtlayıcı yönerge” tuzağını ve istemci tarafı rotalar için sitemap’in nasıl üretileceğini — mi istiyorsunuz? Advanced sekmesine geçin.
TL;DR — Bir SPA’nın temel SEO riski, soyut anlamda JavaScript değil, istemci tarafı yönlendirmenin kullanıcının gördüğünü sunucunun döndüreceği şeyi değiştirmeden değiştirmesidir. Çözümün sürekli birbirine karıştırılan iki bağımsız yarısı vardır: adreslenebilirlik (History API, rota başına benzersiz URL’ler ve
#!parçalarının olmaması) ve içerik kullanılabilirliği (SSR, prerendering/SSG veya meta-framework). Yalnızca ilkini yaparsanız tümü aynı kabuğu oluşturan düzenli bir sitemap elde edersiniz. Her ikisinin üzerine her rotanın oluşturulmuş DOM’da kendi canonical/title/description değerleri olmalı,200döndüren soft-not-found durumları ele alınmalı ve sitemap’teki her rota bağımsız olarak gerçek HTML’ye çözümlenmelidir.
SPA SEO gerçekte nedir
SPA bir uygulama mimarisidir; garanti edilmiş bir SEO başarısızlık durumu değildir. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Sunucu tarafı oluşturma, prerendering veya dikkatle uygulanmış istemci tarafı oluşturma içeriği görünür kılabilir, ancak hiçbiri dizine eklenmeyi garanti etmez. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Bu, özellikle tek sayfalı uygulamaların — React Router, Vue Router veya Angular Router ile istemci tarafı yönlendirmenin — derinlemesine incelenmesidir; ilk yüklemeden sonra sunucu bir daha tam sayfa göndermez. Daha geniş JavaScript SEO rehberinin (parite, lazy-loading, infinite scroll ve JS oluşturmayı genel olarak ele alır) ve React, Next.js, Nuxt, Angular, Vue, Svelte ve Astro için framework rehberlerinin yanında yer alır. Burada yalnızca yönlendirme katmanına ve bunun tarama ile dizine eklemeyi nasıl etkilediğine odaklanıyorum.
Çıplak istemci tarafı yönlendirmeli SPA’lar SEO’da neden başarısız olur
Bir URL, bir HTML yanıtı — app-shell sorunu
Google bu arıza biçimini açıkça tanımlar: “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (Türkçesi: “Bazı JavaScript siteleri, ilk HTML’in gerçek içeriği içermediği ve Google’ın gerçek sayfa içeriğini görebilmek için JavaScript çalıştırması gereken app shell modelini kullanabilir.”) Çıplak bir SPA’de “app shell”, sunucunun döndürdüğü tek şeydir. /products için yeni bir istek yaptığınızda /about için alacağınız aynı neredeyse boş belgeyi alırsınız. İçerik ancak tarayıcı JavaScript’inizi çalıştırıp yönlendiriciniz ne göstereceğine karar verdikten sonra ayrışır.
Sorun tek cümlede budur: istemci tarafı yönlendirme, kullanıcının gördüğünü sunucunun döndüreceği şeyi değiştirmeden değiştirir. Sonraki her şey — soft-not-found durumları, yinelenen içerik, eksik başlıklar — bunun belirtisidir.
Sunucu hangi “sayfanın” istendiğini hiç görmez (hash yönlendirmesi neden başarısız olur)
Eski SPA’lar görünümleri URL parçalarından yüklerdi — example.com/#/products. Google’ın tam ifadesi şöyledir: “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (Türkçesi: “Bir SPA, farklı görünümleri yüklemek için URL parçalarını kullanabilir.”) Bunun başarısız olmasının belirli ve mekanik bir nedeni vardır: tarayıcılar parçayı (# sonrasındaki her şeyi) HTTP isteğinde sunucuya asla göndermez. Sunucu hangi “sayfanın” istendiğini bilemez; bu nedenle farklı içerik veya farklı durum kodu döndüremez. Bir URL’yi bir kez getiren tarayıcı, parça ne olursa olsun aynı HTML’i alır.
Google’ın 2009 tarihli AJAX tarama şemasını Ekim 2015’te resmen kullanımdan kaldırmasının nedeni de budur: “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (Türkçesi: “Kısacası, 2009’da yaptığımız AJAX tarama teklifini artık önermiyoruz.”) Eski _escaped_fragment_ çözümü, parça rotalarını istek üzerine önceden oluşturmaya izin veriyordu; ancak yönlendirme sorununu çözmek yerine etrafından dolaşıyordu. Google’ın bunun yerine önerdiği yöntem aşağıda ele alınan History API’dir.
Soft-not-found durumları — istemci tarafı yönlendiriciler her şey için 200’ü korur
This one is close to inevitable in a bare SPA. Client-side routers, by design, keep the
original page’s 200 status for every virtual navigation — including “not found”
states. Google flags it explicitly: “In a single-page application (SPA), this can be
especially difficult. To prevent error pages from being indexed, you can use one or both
of the following strategies.” And the why: “When a SPA is using client-side JavaScript
to handle errors they often report a 200 HTTP status code instead of the appropriate
status code.”
Sonuç, ince 200 sayfalar olarak dizine eklenen boş veya hata görünümleridir. Google burada tam olarak iki strateji belgeler: sunucusu gerçek bir 404/hata durumu döndüren bir URL’ye yönlendirin (veya tam istek yapın) ya da hata görünümüne JavaScript ile noindex etiketi ekleyin. İkinci seçeneğe dikkat edin: noindex uygulama rotanın geçersiz olduğuna karar vermeden önceki ilk boyamada mevcutsa Google’ın sayfayı oluşturmayı atlamasına yol açabilir. Bu nedenle yalnızca sonradan oluşan DOM’da ne göründüğünü değil, herhangi bir JS çalışmadan önce doğrudan isteğin ne döndürdüğünü doğrulayın. Evidence for this claim For a client-rendered not-found view, Google documents two approaches: redirect to a URL whose server returns a 404 response, or add noindex with JavaScript; an initial noindex can cause rendering to be skipped, so raw and rendered directives require separate testing. Scope: SPAs Confidence: high · Verified: Fix Search-related JavaScript problems
Paylaşılan kabuk kopyaları — olası neden, otomatik teşhis değil
İkinci ve daha sinsi bir arıza biçimi vardır: farklı rotalar, aynı paylaşılan header/nav/footer şablonuna indirgendikleri için birbirinin kopyası olarak dizine eklenebilir. Gary Illyes bunun bir yolunu şöyle anlattı: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (Türkçesi: “Gelen kutumda, sorunun merkez içeriğin yüklenmesinin çok uzun sürmesi ve oluşturmanın zaman aşımına uğraması (en olası açıklamam) olduğunu belirten birçok e-posta var; sonuçta yalnızca şablon içeriği olan bir sürü sayfa kaldı. Yalnızca şablon varken bu sayfalar kopyadır.”) Çözümü de şuydu: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (Türkçesi: “JS çağrılarını, içeriğin (ikincil şablon içeriği dahil) önce yükleneceği şekilde yeniden yapılandırmayı deneyin.”) (Otomatik doğrulamaya direnen bir LinkedIn gönderisi üzerinden aktarıldı; yüksek güvenli ikincil kaynak olarak ele alın.)
Illyes’in bunu doğrulanmış bir teşhis olarak değil, “en olası açıklamam” şeklinde çerçevelediğine dikkat edin — bunu kelimesi kelimesine ciddiye almak gerekir. “Render timeout” tek başına belirtilerden teşhis edilemez; bir rotanın yinelenen içerik sorununu özellikle zaman aşımına bağlamadan önce istek düzeyinde kanıta (doğrudan getirinin gerçekten ne döndürdüğüne), oluşturulmuş çıktı kanıtına (merkez içeriğin oluşturma sonrasında mevcut olup olmadığına) ve Search Console kanıtına (yinelenen/dizine ekleme sinyallerine) ihtiyacınız vardır. Ayrıca tasarım yapılacak sabit, yayımlanmış bir zaman aşımı yoktur — Google’ın temel belgesi bir sayfanın “may stay on this queue for a few seconds, but it can take longer than that,” (Türkçesi: “birkaç saniye bu kuyrukta kalabilir, ancak daha uzun sürebilir”) kuyrukta kalabileceğini söyler; bir sayı taahhüt etmez. Varsayılan bir oluşturma kuyruğu süresine göre plan yapmayın; oluşturmanın ne kadar sürdüğünden bağımsız olarak birincil içeriğin olabildiğince erken yüklenmesini sağlayın.
Çözüm: her rota için gerçek HTML elde edin
Tam çözüm, insanların sürekli birbirine karıştırdığı iki bağımsız parçaya sahiptir:
- URL adreslenebilirliği — History API, rota başına benzersiz URL’ler, parça yok.
- İçerik kullanılabilirliği — SSR, statik prerendering/SSG veya bir meta-framework.
Yalnızca #1’i yaparsanız, hâlâ aynı boş kabuğu döndüren temiz ve paylaşılabilir URL’ler elde edersiniz. Gerçekten dizine eklenmesini istediğiniz bir rota için ikisine de ihtiyacınız vardır.
Bu, her yere SSR eklemeye ilişkin evrensel bir zorunluluk da değildir. SSR ve prerendering, tarayıcının JavaScript’inizi başarıyla çalıştırmasına olan bağımlılığınızı azaltır — site teknik olarak SPA olduğu anda zorunlu hale gelmezler. Belirli bir rota için mimari seçmeden önce üç şeyi kontrol edin: bu rotanın gerçekten sıralanması gerekip gerekmediği (dahili yönetim panelinin gerekmez), JS’siz doğrudan isteğin zaten ne döndürdüğü (bazı kurulumlar anlamlı HTML’i zaten gönderir) ve hangi tarayıcılara hizmet etmeniz gerektiği (Google JS’i oldukça güvenilir biçimde oluşturur; Bing ve çoğu AI tarayıcısı daha az güvenilirdir — aşağıya bakın). Bu kontrolleri zaten geçen CSR, site SPA olduğu için SSR’ye dönüştürülmek zorunda değildir.
Sunucu tarafı oluşturma (SSR)
Sunucu, her istek için uygulamanızı çalıştırır ve bu rota için tamamen oluşturulmuş HTML’i döndürür; ardından istemci onu canlı bir SPA’ye “hydrate” eder. Bu, tarayıcının ilk getirişinde eksiksiz içerik aldığı ve JS çalıştırması gerekmediği için en sağlam seçenektir.
Statik prerendering / SSG
İstek başına oluşturmak yerine deploy sırasında her rotanın HTML’ini önceden derlersiniz. Kullanıcıya göre değişmeyen içerikler için idealdir. Kendi JavaScript SEO yazılarımda da söylediğim gibi, her tür SSR, statik oluşturma ve prerendering kurulumu arama motorları için uygundur — kaçınılması gereken şey içeriği yalnızca istemci tarafı oluşturmanın arkasında bırakmaktır.
Veya: kendiniz yazmayın — bir meta-framework kullanın
Yeni bir proje için dürüst önerim, React-Router-only veya Vue-Router-only istemci tarafı yönlendirmeyi baştan kendiniz yazmamanızdır. Next.js, Nuxt, SSR paketiyle Angular, SvelteKit ve Remix gibi bir meta-framework, SSR/SSG’yi ve rota tabanlı meta verileri kutudan çıktığı haliyle sunar; böylece bu sorun kategorisinin tamamını aşar. (Bu sitede bunların her biri için ayrı derinlemesine rehber vardır.) Diğer taraf konusunda dürüst olun: mevcut, elle yazılmış bir SPA’ye SSR sonradan eklemek gerçek mühendislik işidir — bir yapılandırma düğmesi değil, bir geçiş projesidir.
Hedef değil, geçici çözüm olarak dinamik oluşturma
Tarayıcılara ayrı oluşturulmuş bir HTML anlık görüntüsü sunabilirsiniz (dynamic rendering). Hem Google hem Bing bunu kabul eder — Bing, “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (Türkçesi: “dynamic rendering’ı JavaScript’e yoğun biçimde dayanan web siteleri için harika bir alternatif olarak önerir”) (Bing’in 2018 blogundan aktarıldı; aşağıdaki iki kısa bingbot-yeteneği alıntısı doğrudan doğrulanmıştır, bu uzun ifade bağımsız olarak tekrar kontrol edilmemiştir) — ancak Google bunun bir geçici çözüm olduğunu açıkça söyler: “Dynamic rendering was a workaround and not a long-term solution… Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.” (Türkçesi: “Dynamic rendering bir geçici çözümdü, uzun vadeli bir çözüm değildi… Bunun yerine sunucu tarafı oluşturma, statik oluşturma veya hydration kullanmanızı öneririz.”) (Google’ın dynamic-rendering belgesinden aktarıldı; ifade bu sitenin daha geniş JavaScript SEO rehberinde zaten alıntılanan metinle örtüşür.) Bu bir köprüdür, mimari değildir.
History API ile hash yönlendirmesi (#!)
İki değil, beş durum
Bir SPA rotasını test etmek kafa karıştırır; çünkü “çalışıyor mu” aslında ayrı ayrı doğrulanabilen beş farklı duruma uzanır:
| Durum | Nedir? |
|---|---|
| Sunucu yanıtı | JS’siz, yeni bir HTTP isteğinin URL’den gerçekten aldığı baytlar. |
| Oluşturulmuş DOM | Bir tarayıcının (veya Googlebot oluşturucusunun), sunucu yanıtı üzerinde JavaScript çalıştırdıktan sonra oluşturduğu şey. |
| Arama işlemesi | Google’ın sunucu yanıtını ayrı ayrı taraması, daha sonra sayfayı oluşturması ve ikisine dayanarak dizine eklemesi. |
| Tarayıcıda tam belge gezinmesi | Bir URL’ye yapılan gerçek yeni HTTP isteği — sunucu yanıtını veya durum kodunu değiştirebilen tek durum. |
| Tarayıcıda soft navigation | Görünen URL’yi, tarayıcı geçmişini ve ekrandaki arayüzü değiştiren bir History API geçişi (pushState/replaceState). |
Herkesin birbirine karıştırdığı nokta şudur: soft navigation URL’yi ve arayüzü değiştirir, ancak tek başına yeni bir HTTP yanıtı veya durum kodu oluşturmaz — bu yalnızca tam gezinmede (veya curl gibi eşdeğer doğrudan istekte) gerçekleşir. Ana sayfadan uygulamaya tıklayarak bir rotayı test etmek soft navigation’ı; URL’yi doğrudan istemek sunucu yanıtını test eder. İkisi de önemlidir ve birbiriyle uyuşmayabilir.
History API size ne verir
Google’ın önerisi açıktır: “We recommend using the History API to load different content based on the URL in a SPA.” (Türkçesi: “Bir SPA’de URL’ye göre farklı içerik yüklemek için History API’yi kullanmanızı öneririz.”) (Canlı belgede “History API” bir bağlantı olduğundan bu deep link giriş cümlesini hedefler.) History API (pushState/replaceState), tam yükleme olmadan yönlendiricinizin görünen URL’yi gerçek ve yer imlerine eklenebilir bir yola — /products, /#/products değil — değiştirmesini sağlar. Bu, adreslenebilirlik yarısını düzeltir: artık her görünümün, sunucunun farklı yanıt verebileceği bir URL’si vardır.
Parça/hash URL’leri sunucuya — ve Google’a — neden görünmez
Parça sunucuya asla ulaşmadığı için (yukarıya bakın), History API her rotaya sunucunun gerçekten hizmet verebileceği bir URL vermenin tek yoludur. Google, temel belgesinde bunun alt sınırını şöyle çizer: “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (Türkçesi: “Farklı sayfa içerikleri yüklemek için parçaları kullanmayın. Aşağıdaki örnek kötü bir uygulamadır; çünkü Googlebot URL’leri güvenilir biçimde çözemez.”) Bu, başka yerde yazdığım önerinin aynısıdır — /products gibi hash URL’leri değil, /#/products gibi normal görünen URL’leri kullanın; Google hash olanları güvenilir biçimde dizine ekleyemez.
2015’teki kullanımdan kaldırma, kısaca
Hash-bang (#!) dönemi Google’ın 2015 tarihli gönderisiyle sona erdi: “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers,” (Türkçesi: “Zaman değişti. Bugün, Googlebot’un JavaScript veya CSS dosyalarınızı taramasını engellemediğiniz sürece web sayfalarınızı genellikle modern tarayıcılar gibi oluşturup anlayabiliyoruz.”) ve “you can use the History API pushState() to ensure accessibility for a wider range of browsers (and our systems).” (Türkçesi: “Daha geniş bir tarayıcı yelpazesi (ve sistemlerimiz) için erişilebilirliği sağlamak üzere History API pushState() kullanabilirsiniz.”) Korunması gereken bir nüans şudur: Google eski hash-bang sitelerini anında dizinden çıkarmadı — “we’ll generally crawl, render, and index the \u0060#!\u0060 URLs” (Türkçesi: “\u0060#!\u0060 URL’lerini genellikle tarayacak, oluşturacak ve dizine ekleyeceğiz.”) — ancak “hâlâ deneyebiliriz”, “bunu hâlâ yapmalısınız” demek değildir.
Her rotayı bağımsız olarak dizine eklenebilir kılma
Temiz URL’ler ve gerçek HTML taranmanızı sağlar. Doğru biçimde dizine eklenmek için her rotanın kendi sinyallerine ihtiyacı vardır.
Rota başına canonical etiketleri — “en kısıtlayıcı yönerge” tuzağı
Her rotanın oluşturulmuş DOM’da kendi rel=canonical değerine sahip olması gerekir. Tuzak şudur: ham HTML kabuğunda bir placeholder canonical (veya noindex) gönderilir ve JavaScript’in daha sonra üzerine yazması beklenirse çakışma oluşabilir. Google ham ve oluşturulmuş sürümler arasındaki çakışmaları daha kısıtlayıcı sinyali alarak çözer — daha önce ifade ettiğim gibi Google, bir sayfanın HTML’i ile oluşturulmuş sürümü arasındaki en kısıtlayıcı ifadeleri seçer. Kabuğa gömülmüş başıboş bir noindex veya yanlış canonical, JS’iniz bunu “düzelttikten” sonra bile tüm rotayı sessizce bastırabilir.
JavaScript ile rota başına title ve meta description
Bunları JS ile ayarlamak uygundur — Google bunu doğrudan söyler: “You can use JavaScript to set or change the meta description as well as the <title> element.” (Türkçesi: “meta description’ın yanı sıra <title> öğesini ayarlamak veya değiştirmek için JavaScript kullanabilirsiniz.”) Gereklilik, bunların kısa süreliğine görünmesi değil, Google’ın değerlendirdiği oluşturulmuş DOM’a yerleşmesidir. Her sayfaya rota değiştiğinde güncellenen kendi başlığını ve açıklamasını verin.
Rotalar arasında gerçek <a href> bağlantıları
Rotalar arası bağlantılarınızı href üzerindeki tıklama işleyicileriyle değil, <div> öznitelikli gerçek anchor’larla kurun. Google URL’leri href değerlerini çıkararak keşfeder; router üzerinden yönlendiren bir <div onClick>, tarayıcının bağlantı çıkarma işlemine görünmez. Bu gezinmeler arasında içeriği taşımak için istemci tarafı durumuna da güvenmeyin — Google’ın oluşturucusu “does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (Türkçesi: “Sayfa yüklemeleri arasında durumu korumaz: Local Storage ve Session Storage verileri sayfa yüklemeleri arasında temizlenir. HTTP çerezleri sayfa yüklemeleri arasında temizlenir.”)
SPA rotaları için sitemap üretimi
Özel bir “SPA sitemap” biçimi yok — mesele muhasebedir
SPA’lar için sitemap protokolü değişmez. Asıl iş, listelediğiniz her rotanın bağımsız olarak gerçek, benzersiz ve oluşturulmuş HTML’e çözülmesini sağlamaktır. İstemci tarafındaki 500 rota aynı kabuğu döndürüyorsa bu rotaların sitemap’i değersizdir. Dolayısıyla sitemap, oluşturma stratejinizin alt ürünüdür; onun yerine geçmez.
Dinamik/parametreli rotaları ele alma (/product/:id)
Parametreli rotaları olan uygulamalarda listeyi elle sürdüremezsiniz. Sitemap oluşturucusu, uygulamanın kullandığı veri kaynağına karşı çalışmalıdır — her id’yi sıralayan bir build script’i veya sunucu endpoint’i — böylece sitemap ile uygulama birbirinden kopmaz. Sıralanan URL’ler ancak SSR’den veya prerendering’den geçtiğinde listelenmeye değerdir.
Sitemap’i sunucunun çözebildiği şeyle senkron tutma
İçeriğinizin kaynağına bağlı olarak sitemap’i derleme sırasında veya içerik kaynağınıza bağlı bir takvimle yeniden oluşturun. Sitemap’te 200 döndüren (veya daha kötüsü soft-not-found olan) bir rota bulunması istemediğiniz bir tarama bütçesi ve kalite sinyalidir.
Google’ın gerçekte ne gördüğünü test etme
Tek bir URL’yi noktasal olarak kontrol etmeyin. SPA sorununun özü, rotaların tarayıcıda farklılaşıp ağ üzerinde farklılaşmayabilmesidir; bu nedenle birden çok rotayı ham HTML düzeyinde test edin:
- Her rota için ham HTML’i getirin; uygun olduğunda bir Googlebot user-agent’ı ile
curlkullanın ve içeriğin paylaşılan kabuk değil, URL başına benzersiz olduğunu doğrulayın. - Search Console’da URL Inspection — birkaç rotanın taranan/oluşturulan HTML’ini karşılaştırın ve rota başına title, description ve canonical değerlerinin bulunduğunu doğrulayın.
- Hata rotalarının doğru sinyali döndürdüğünü doğrulayın — “bulunamadı” görünümü ya gerçek bir hata durumuna yönlenmeli ya da oluşturulmuş DOM’da
noindextaşımalıdır.
SPA SEO hakkında yaygın mitler
- “Google SPA’leri hiç dizine ekleyemez.” Güncelliğini yitirmiştir. Google evergreen Chromium çalıştırır ve genellikle SPA içeriğini oluşturur. Gerçek riskler belirgindir: soft-not-found durumları, hash routing, oluşturma zaman aşımları ve Google dışı tarayıcılar (Bing daha az güvenilir, çoğu AI tarayıcısı hiç oluşturmaz).
- “History API eklemek SPA SEO’sunu düzeltir.” Yalnızca adreslenebilirliği düzeltir. Sunucu her rota için hâlâ aynı kabuğu döndürüyorsa Google bir şey görebilmek için yine JS çalıştırmak zorundadır.
- “Hash routing hâlâ yedek olarak çalışır.” 2015’ten beri kullanım dışıdır ve o zamandan beri önerilmemektedir.
- “İstemci tarafı oluşturma bir sıralama cezasıdır.” Doğrudan bir CSR cezası yoktur. Zarar dolaylıdır: başarısız/gecikmiş oluşturma, soft-not-found durumları ve oluşturma zaman aşımlarından kaynaklanan yinelenen kümeler dizine eklenen içeriği azaltır.
- “Botlar için önceden oluşturma cloaking’dir.” İçerik kullanıcıların sonunda gördüğü içerikle eşleştiğinde cloaking değildir; farklı olan yalnızca içeriğin ne zaman ve nerede oluşturulduğudur.
- “Özel bir SPA sitemap oluşturucusuna ihtiyacınız var.” Özel bir format yoktur; yapılması gereken, listelenen her rotanın gerçek HTML’ye çözümlenmesini sağlamaktır.
SSS
Google tek sayfalı bir uygulamayı dizine ekleyebilir mi? Evet; her rota gerçek bir URL’de gerçek ve benzersiz HTML’e (SSR/prerendering yoluyla) çözülüyorsa. Çıplak, yalnızca istemci tarafında çalışan SPA’lar çoğu zaman bunu yapamaz.
SSR’ye ihtiyacım var mı, yoksa istemci tarafı oluşturma hiç uygun olabilir mi? Sıralanması gerekmeyen içerik için CSR çalışabilir; ancak güvenilir biçimde dizine eklenmesini istediğiniz her şey için prerendering veya SSR kullanın — oluşturucunun JS’inizi zamanında çalıştırmasına bel bağlamayın.
Hash (#!) yönlendirmesi SEO için kötü mü? Evet — parça sunucuya hiç ulaşmaz; bu yüzden Google bu URL’leri güvenilir biçimde çözemez. History API’yi kullanın.
Her rotanın kendi canonical etiketine ihtiyacı var mı? Evet, oluşturulmuş DOM’da — ve ham kabukta daha kısıtlayıcı hiçbir şeyin (başıboş bir noindex veya yanlış canonical) gönderilmediğinden emin olun.
Bir prerendering servisi cloaking midir? Hayır; botlara sunulan içerik kullanıcıların gördüğü içerikle eşleştiği sürece.
Yönlendirmeyi kendim kurmak yerine bir meta-framework kullanmalı mıyım? Yeni bir build için genellikle evet — Next.js/Nuxt/Angular-SSR/SvelteKit size SSR/SSG’yi ve rota başına meta verileri ücretsiz sunar.
SPA’m var olmayan sayfalar için neden 200 döndürüyor? Çünkü istemci tarafı yönlendirici sanal gezinmelerde başlangıçtaki 200 durumunu korur. Gerçek hata durumuna bir JS yönlendirmesiyle veya oluşturulmuş bir noindex ile düzeltin.
AI özeti
Advanced sürümün özeti:
- SPA SEO = özellikle istemci tarafı yönlendirme (React Router, Vue Router, Angular Router). SPA, JavaScript ile bir belge yükleyip görünümleri değiştirmesiyle tanımlanır — her URL için neredeyse boş HTML gönderen bir “app shell” yaygın bir uygulamadır, her SPA’nın uyması gereken bir kural değildir; varsayımda bulunmak yerine doğrudan isteğin gerçekte ne döndürdüğünü kontrol edin.
- Temel risk: istemci tarafı yönlendirme, kullanıcının gördüğünü sunucunun döndüreceği şeyi değiştirmeden değiştirebilir. Çıplak bir app-shell SPA’da
/productsve/aboutiçin doğrudan yapılan istekler bayt düzeyinde aynı ham HTML’yi döndürebilir. - Beş durum birbirine karıştırılır: sunucu yanıtı, oluşturulmuş DOM, Arama işleme süreci, tarayıcıda tam belge gezinmesi ve tarayıcıda soft navigation. History API geçişi URL’yi ve arayüzü değiştirir; ancak kendi başına yeni bir sunucu yanıtı veya durum oluşturmaz.
- Çıplak kabuğun üç belirtisi: app-shell sorunu; soft-not-found durumları (yönlendiriciler “not found” için
200’ü korur — Google’ın belgelenmiş iki çözümü, kendisi 404s döndüren bir URL’ye yönlendirmek veya JS ile eklenen birnoindex’tir; başlangıçtaki birnoindexoluşturmanın atlanmasına neden olabilir); ve paylaşılan-kabuk kopyaları — “render timeout” (Illyes) bunların olası bir nedenidir, ancak istek/oluşturma/Arama kanıtı olmadan otomatik teşhis değildir. - Hash routing mekanik olarak başarısız olur:
#sonrasındaki parça sunucuya hiç ulaşmaz; bu nedenle sunucu rotaya göre farklı davranamaz. Google tarafından 2015’te kullanım dışı bırakılmıştır; History API adreslenebilirlik sağlar, eksiksiz bir dizine eklenebilirlik çözümü değil. - Çıplak kabuk sorunsa çözümün iki bağımsız yarısı vardır: adreslenebilirlik (History API, rota başına benzersiz URL’ler) ve içerik kullanılabilirliği (SSR, prerendering/SSG veya meta-framework); ancak SSR evrensel bir gereklilik değildir. Kararı rotanın sıralanması gerekip gerekmediğine, doğrudan ne döndürdüğüne ve hangi tarayıcıların oluşturma yapması gerektiğine göre verin.
- Rota başına: oluşturulmuş DOM’da kendi canonical/title/description değeriniz olsun; ham HTML’deki bir
noindex/canonical’ın JS’nizin sinyalini geçersiz kıldığı “en kısıtlayıcı yönerge” tuzağına dikkat edin. Gerçek<a href>bağlantıları kullanın; istemci durumuna güvenmeyin (WRS yüklemeler arasında depolama/çerezleri temizler). - Sitemap’ler: bir rotayı listelemek onun çözümlendiğini kanıtlamaz; özel bir format yoktur. Listelenen her rota bağımsız olarak gerçek HTML’ye çözümlenmeli, dinamik rotalar da uygulamanın veri kaynağına bağlı bir oluşturucu kullanmalıdır.
- Dynamic rendering, Google/Bing tarafından kabul edilen geçici bir çözümdür; uzun vadeli mimari değildir.
- Birden çok rotayı test edin: yalnızca uygulama içinde gezinerek değil, ham HTML düzeyinde (doğrudan isteklerle) test edin.
Resmî belgeler
Arama motorlarının birincil kaynak belgeleri.
- Understand the JavaScript SEO basics — app-shell modeli, “parçalar yerine History API kullanın” ve başlık/açıklamaları JS ile ayarlama.
- Fix Search-related JavaScript problems — SPA soft-404 bölümü, “farklı içerik yüklemek için URL parçalarını kullanmayın” ve History API önerisi.
- Dynamic Rendering as a workaround — dynamic rendering’ın neden geçici bir çözüm, uzun vadeli bir çözüm olmadığını açıklar.
- Deprecating our AJAX crawling scheme (2015) — hash-bang taramasının resmî sona ermesi ve pushState önerisi.
- Build and submit a sitemap — genel sitemap protokolü (SPA’ya özel bir biçim yoktur).
Bing / Microsoft
- bingbot Series: JavaScript, Dynamic Rendering, and Cloaking. Oh My! — bingbot’un JS oluşturma yetenekleri ve dynamic rendering tutumu.
Kaynaktan alıntılar
Google ve Bing’den kayda geçmiş ifadeler. Her bağlantı, kaynak sayfasındaki alıntı bölümüne atlayan bir deep link’tir.
Google — app-shell sorunu
- “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (Türkçesi: “Bazı JavaScript siteleri, ilk HTML’in gerçek içeriği içermediği ve Google’ın gerçek sayfa içeriğini görebilmek için JavaScript çalıştırması gereken app shell modelini kullanabilir.”) Jump to quote
Google — hash/parça yönlendirmesi ve History API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (Türkçesi: “Bir SPA, farklı görünümleri yüklemek için URL parçalarını kullanabilir.”) Jump to quote
- “We recommend using the History API to load different content based on the URL in a SPA.” (Türkçesi: “Bir SPA’de URL’ye göre farklı içerik yüklemek için History API’yi kullanmanızı öneririz.”) Jump to quote (Canlı belgede “History API” bir bağlantı olduğundan bu deep link giriş cümlesini hedefler; tam cümle sırada kelimesi kelimesine bulunur.)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (Türkçesi: “Farklı sayfa içerikleri yüklemek için parçaları kullanmayın. Aşağıdaki örnek kötü bir uygulamadır; çünkü Googlebot URL’leri güvenilir biçimde çözemez.”) Jump to quote
Google — SPA soft 404s
- “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” Jump to quote
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” Jump to quote
Google — JavaScript ile meta etiketleri ve durumsuz oluşturma
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (Türkçesi: “meta description’ın yanı sıra<title>öğesini ayarlamak veya değiştirmek için JavaScript kullanabilirsiniz.”) Jump to quote - “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (Türkçesi: “WRS sayfa yüklemeleri arasında durumu korumaz: Local Storage ve Session Storage verileri sayfa yüklemeleri arasında temizlenir. HTTP çerezleri sayfa yüklemeleri arasında temizlenir.”) Jump to quote
Google — hash-bang taramasını kullanımdan kaldırma (2015)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (Türkçesi: “Kısacası, 2009’da yaptığımız AJAX tarama teklifini artık önermiyoruz.”) Jump to quote
- “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.” (Türkçesi: “Zaman değişti. Bugün, Googlebot’un JavaScript veya CSS dosyalarınızı taramasını engellemediğiniz sürece web sayfalarınızı genellikle modern tarayıcılar gibi oluşturup anlayabiliyoruz.”) Jump to quote
Google — dynamic rendering bir geçici çözümdür (aktarıldı; ifade bu sitenin daha geniş JavaScript SEO rehberinde zaten alıntılanan metinle örtüşür, bu geçişte bağımsız olarak yeniden doğrulanmadı)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (Türkçesi: “Dynamic rendering, arama motorlarında JavaScript ile oluşturulan içerik sorunları için bir geçici çözümdü; uzun vadeli bir çözüm değildi.”) Jump to quote
Bing — bingbot ve JavaScript
- “bingbot is generally able to render JavaScript.” (Türkçesi: “bingbot genellikle JavaScript oluşturabilir.”) — bingbot Series, Bing Webmaster Blog. Read the post
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (Türkçesi: “bingbot, en sevdiğiniz modern tarayıcının son sürümünün desteklediği JavaScript framework’lerinin tümünü zorunlu olarak desteklemez.”) — aynı gönderi. Read the post
Gary Illyes, Google — oluşturma zaman aşımları kopyalar oluşturur (otomatik doğrulamaya direnen bir LinkedIn gönderisi üzerinden aktarıldı; yüksek güvenli ikincil kaynak olarak ele alın)
- “the centerpiece took forever to load, so rendering timed out… and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (Türkçesi: “merkez içeriğin yüklenmesi çok uzun sürdü, bu yüzden oluşturma zaman aşımına uğradı… ve yalnızca şablon içeriği olan bir sürü sayfayla kaldık. Yalnızca şablon varken bu sayfalar kopyadır.”) Read the post
Hangi oluşturma yolunu seçmeliyim?
Yukarıdan aşağıya ilerleyin. Soru her zaman şudur: “Tarayıcı, JS’imi çalıştırmadan bu rota için gerçek HTML alıyor mu?” — rotanın gerçekten dizine eklenmesi gerekip gerekmediğini doğrulayarak ve JS’siz doğrudan isteğin zaten ne döndürdüğünü kontrol ederek başlayın; her rota SSR gerektirmez ve bazıları zaten kullanılabilir HTML döndürür.
1. Yeni bir build’e mi başlıyorsunuz?
- Evet → Bir meta-framework kullanın (Next.js, Nuxt, SSR ile Angular, SvelteKit, Remix). SSR/SSG ve rota başına meta veriler yerleşik gelir. Burada durun.
- Hayır → Devam edin.
2. İçeriğiniz kullanıcıya / isteğe göre değişiyor mu?
- Hayır (çoğunlukla statik içerik) → Build sırasında prerender / SSG. En basit sağlam çözüm — her rota diskte gerçek HTML’dir.
- Evet → Her isteğin rotaya özgü HTML döndürmesi için sunucu tarafı oluşturma (SSR).
3. Şu anda SSR veya prerendering yapamıyor musunuz (eski, elle yazılmış SPA)?
- Dynamic rendering’ı geçici bir köprü olarak kullanın — tarayıcılara oluşturulmuş bir anlık görüntü sunun. SSR/SSG’ye geçişi planlayın; bunu hedef mimari olarak görmeyin.
4. Hangi yolu seçerseniz seçin, rota başına aşağıdakilerin tümünü doğrulayın:
- History API üzerinden gerçek URL (
#!parçaları yok). - Oluşturulmuş DOM’da benzersiz title + meta description + canonical.
- Ham kabuğa gömülmüş daha kısıtlayıcı hiçbir sinyal yok (
noindex, yanlış canonical). - Rotalar arasında gerçek
<a href>bağlantıları. - Hata rotaları gerçek bir hata durumu veya oluşturulmuş
noindexdöndürür (soft-not-found yok). - Sitemap’teki her rota bağımsız olarak gerçek HTML’ye çözümlenir.
SPA SEO kontrol listesi
Adreslenebilirlik
- Her görünüm, parça (
/products) yerine History API üzerinden gerçek bir URL’ye (/#/products) sahip. - Rotalar arası bağlantılar
<a href>işleyicileri değil, gerçek<div onClick>anchor’ları.
İçerik kullanılabilirliği
- Her rota, tarayıcının JS çalıştırmasına gerek kalmadan gerçek ve benzersiz HTML döndürüyor (SSR, prerendering/SSG veya meta-framework).
- Rota içeriği oluşturma penceresi kapanmadan yükleniyor (yalnızca şablon içeren kopyalardan kaçının).
Rota başına dizine eklenebilirlik
- Rota başına benzersiz
<title>ve meta description oluşturulmuş DOM’da mevcut. - Rota başına benzersiz
rel=canonicaloluşturulmuş DOM’da mevcut. - Daha kısıtlayıcı bir sinyalin kilitleyebileceği başıboş
noindex/placeholder canonical ham kabukta yok.
Hatalar
- “Bulunamadı” rotaları gerçek bir hata durumu döndüren bir sunucu URL’sine yönlenir veya JavaScript tarafından eklenen
noindextaşır (noindexile200’de soft-not-found yok). -
noindexrotasını kullanıyorsanız, bunun rota geçersiz olduğuna karar verildikten sonra eklendiğini, ilk boyamadan itibaren mevcut olmadığını doğrulayın — başlangıçtakinoindex, Google’ın sayfayı oluşturmayı atlamasına neden olabilir.
Sitemap
- Listelenen her rota bağımsız olarak gerçek HTML’e çözülüyor.
- Dinamik rotalar (
/product/:id) elle değil, uygulamanın veri kaynağından sıralanıyor.
Doğrulama
- Benzersiz içeriği URL başına doğrulayan ham HTML, birden çok rota üzerinde (curl) kontrol edildi.
- URL Inspection, her rota için oluşturulmuş içeriği, title’ı, description’ı ve canonical’ı doğruluyor.
Zihinsel modeller
1. Birbirine karıştırmamanız gereken iki yarı. Adreslenebilirlik (History API, benzersiz URL’ler, parça yok) ile içerik kullanılabilirliği (SSR, prerendering veya meta-framework) bağımsızdır. Rota başına HTML olmadan temiz URL’ler = boş kabuklardan oluşan düzenli bir sitemap. İkisine de ihtiyacınız vardır.
2. “Sunucu taze bir istekte ne döndürürdü?”
SPA sorununun tamamı, tarayıcı görünümünün sunucu yanıtından ayrışmasıdır. Her rota için JS çalıştırmayan bir curl isteğinin ne döndürdüğünü sorun. Kabuğu döndürüyorsa tarayıcı da kabuğu görebilir.
3. Hatalar dahil varsayılan durum kodu 200’dür.
İstemci tarafı yönlendiriciler başlangıçtaki 200 durumunu korur. Gerçek bir hata durumu veya oluşturulmuş noindex döndürmesini kasıtlı olarak sağlamadığınız sürece her “hata” görünümünü soft 404 kabul edin.
4. En kısıtlayıcı yönerge tuzağı.
Google ham ve oluşturulmuş sürümleri daha kısıtlayıcı sinyali alarak uzlaştırır. Kabuğa konmuş bir noindex veya yanlış canonical, JS “düzeltmenizi” geçersiz kılabilir. Yalnızca oluşturulmuş DOM’u değil, ham HTML’i de denetleyin.
5. Yeni build’ler için önce meta-framework. İstemci tarafı yönlendirmeyi kendiniz yazmak, bu sorunların her birini sahiplenmek demektir. SSR/SSG ve rota başına meta verileri yapan bir framework seçmek, bunları yaşamamayı seçmektir.
SPA SEO hızlı başvuru
Yönlendirme
| Yaklaşım | URL örneği | Sunucuya ulaşır mı? | Google için güvenli mi? |
|---|---|---|---|
| History API | /products | Evet | Evet (önerilir) |
| Hash yönlendirmesi | /#/products | Hayır (parça hiç gönderilmez) | Hayır — 2015’te kullanımdan kaldırıldı |
Hash-bang (#!) | /#!/products | Hayır | Hayır — eski AJAX şeması, kullanımdan kaldırıldı |
Oluşturma stratejileri
| Strateji | Tarayıcı JS çalıştırmadan gerçek HTML alır mı? | En iyi kullanım |
|---|---|---|
| SSR | Evet | Kullanıcı/istek başına değişen içerik |
| Prerendering / SSG | Evet | Çoğunlukla statik içerik |
| Meta-framework (Next/Nuxt/vb.) | Evet (yerleşik) | Yeni build’ler |
| Dynamic rendering | Evet, yalnızca tarayıcılar için | Eski SPA’larda geçici köprü |
| Çıplak CSR (yalnızca istemci) | Hayır | Güvenilir biçimde dizine eklenmesini istediğiniz hiçbir şey |
Rota başına zorunlular (tümü oluşturulmuş DOM’da)
- Benzersiz URL (History API) · benzersiz
<title>· benzersiz meta description · benzersizrel=canonical· gerçek<a href>bağlantıları · gerçek durum veyanoindexdöndüren hata rotaları.
Hızlı bilgiler
#sonrasındaki parça sunucuya asla gönderilmez — hash routing’in başarısız olmasının nedeni budur.- İstemci tarafı yönlendiriciler, “not found” dahil her şey için
200’ü korur → soft-not-found durumları. - Google, ham ve oluşturulmuş sinyalleri daha kısıtlayıcı olanı esas alarak uzlaştırır.
- Özel SPA sitemap formatı yoktur — listelenen her rota gerçek HTML’ye çözümlenmelidir.
SPA SEO anti-pattern’leri
Çıplak bir CSR SPA gönderip tam sitemap’i sunmak. Sitemap 500 rotayı listeler; taze bir istekte 500’ünün tamamı aynı kabuğu döndürür. Sitemap içeriği var etmez — oluşturma var eder.
History API’yi ekleyip işi bitmiş saymak. Temiz URL’ler adreslenebilirliği düzeltir, içeriği değil. SSR/prerendering yoksa sunucu her rota için hâlâ kabuğu döndürür.
Yedek olarak hash / hash-bang yönlendirmesi. Parça sunucuya ulaşmaz; dolayısıyla sunucu rotaya göre farklılaşamaz. 2015’ten beri kullanımdan kaldırılmıştır; yedek değil, çıkmaz sokaktır.
<div onClick> yerine <a href> ile gezinmek.
Google URL’leri keşfetmek için href değerlerini çıkarır. Tıklama işleyicisiyle yönlendirme, bağlantı çıkarma işlemine görünmez; bu rotalar hiç bulunamayabilir.
“JS üzerine yazacak” diye ham kabuğa placeholder noindex veya canonical koymak.
Google ham ve oluşturulmuş sürümlerin daha kısıtlayıcı olanını seçer. Kabuğun yönergesi kazanabilir ve rotayı sessizce bastırabilir.
“Bulunamadı” görünümleri için 200 döndürmek.
Soft-not-found durumları ince/boş sayfaların dizine eklenmesine neden olur. Gerçek bir hata durumuna yönlendirin veya oluşturulmuş bir noindex ekleyin.
Rota içeriğini şablondan sonra yavaş yüklemek. Oluşturma zaman aşımları yalnızca paylaşılan kabuğu bırakır ve her rota diğerinin kopyasına dönüşür (Illyes). Merkez içeriği önce yükleyin.
Gezinmeler arasında içerik taşımak için istemci durumuna güvenmek. Oluşturucu, sayfa yüklemeleri arasında Local/Session Storage’ı ve çerezleri temizler — yalnızca istemci durumunda bulunan içerik, Google sonraki rotayı oluşturduğunda orada olmaz.
Yaygın SPA dizine ekleme sorunları
Her rota aynı HTML’i döndürüyor
Belirti: /products ve /about tarayıcıda farklı görünüyor ancak ham yanıtları aynı. Olası neden: History API yönlendirmesi SSR veya prerendering olmadan adreslenebilirlik sağlıyor. Çözüm: Rota özgü HTML üretin ve her yola doğrudan yapılan isteğin kendi başlığını, gövde metnini ve meta verilerini içerdiğini doğrulayın.
Eksik rotalar başarılı sayfalar olarak görünüyor
Belirti: Var olmayan bir rota, 200 döndürürken bulunamadı görünümünü gösteriyor. Olası neden: Sunucu başarılı bir kabuk gönderdikten sonra hata istemci yönlendiricisinin sorumluluğunda kalıyor. Çözüm: Doğru sunucu durumunu döndürün; bu henüz gönderilemiyorsa gerçek hata durumu olan bir URL’ye yönlendirin veya noindex oluşturun. Uygulama içi gezinmeyle değil, taze ve doğrudan istekle doğrulayın.
Rotalar oluşturma sonrasında kopyalara dönüşüyor
Belirti: Arama motorları farklı URL’leri kümeliyor veya yalnızca paylaşılan gezinmeyi koruyor. Olası neden: Rota içeriği çok geç yükleniyor ve oluşturma şablon içeriğini yakalıyor. Çözüm: Sunucu yanıtında veya en erken oluşturma yolunda birincil içeriğe öncelik verin. İsteğe bağlı script’ler bitmeden önce birkaç rotanın benzersiz içerik sunduğunu doğrulayın.
Sunucunun rota başına gerçekte ne döndürdüğünü test etme
SPA tuzağı, rotaların tarayıcıda farklılaşıp ağ üzerinde farklılaşmamasıdır. Tek bir rota değil, birden çok rotayı ham HTML düzeyinde kontrol edin.
Rota başına ham HTML’yi getirin (macOS / Linux)
# Fetch a few routes with a Googlebot UA and compare — they should NOT be identical
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
for path in / /products /about /product/123; do
echo "=== $path ==="
curl -s -A "$UA" "https://example.com$path" | wc -c # byte counts should differ
doneİki rotanın ham HTML’sini karşılaştırın (aynı kabuk mu?)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
diff <(curl -s -A "$UA" https://example.com/products) \
<(curl -s -A "$UA" https://example.com/about) \
&& echo "IDENTICAL — bare shell, content is client-only" \
|| echo "Different — routes return distinct HTML (good)"Ham yanıtta rota başlığını ve canonical’ı kontrol edin (grep / regex)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -s -A "$UA" https://example.com/products \
| grep -Eio '<title>[^<]*</title>|<link[^>]+rel=["'"'"']canonical["'"'"'][^>]*>'“Bulunamadı” rotasının HTTP durumunu kontrol edin (soft-404 denetleyicisi)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existTarayıcı DevTools konsolunda oluşturulmuş title/canonical’ı inceleyin
// Run on each route after client-side navigation to confirm JS set them
console.log('title:', document.title);
console.log('description:',
document.querySelector('meta[name="description"]')?.content);
console.log('canonical:',
document.querySelector('link[rel="canonical"]')?.href);Bookmarklet — tıklama işleyicili “bağlantıları” gerçek anchor olmayanlar olarak işaretleyin
javascript:(()=>{const bad=[...document.querySelectorAll('[onclick],div[role="link"]')]
.filter(e=>!e.closest('a[href]'));
bad.forEach(e=>e.style.outline='3px solid red');
alert(bad.length+' non-anchor clickable(s) outlined — these are invisible to link extraction');})();XPath — find fragment/hash links a crawler can’t resolve (paste into DevTools console)
$x('//a[starts-with(@href, "#") or contains(@href, "/#/")]')
.map(a => a.getAttribute('href'));
// Any results are hash-routed links; migrate them to History API paths. SPA denetimi için araçlar
- URL Inspection (Google Search Console) — bir rota için taranan ve oluşturulan HTML’i görün; rota başına title, description ve canonical’ın gerçekten yerleştiğini doğrulayın.
- Rich Results Test / URL Inspection “View crawled page” — tek bir URL’nin Google oluşturmasını görün; içeriğin oluşturma sonrasında kaldığını doğrulamak için yararlıdır.
- Googlebot UA ile
curl— birkaç rotanın ham HTML’ini karşılaştırmanın ve paylaşılan kabuğu yakalamanın en hızlı yolu. - Screaming Frog SEO Spider (JavaScript rendering mode) — her rotadaki ham ve oluşturulmuş içerik ile başlıkları karşılaştırmak için siteyi oluşturma olmadan ve oluşturmayla tarayın.
- Ahrefs Site Audit — dizine eklenebilirlik sorunlarını, eksik/yinelenen title ve canonical’ları ve rotalar arasındaki soft-404 benzeri kalıpları gösterir.
- DebugBear — SPA odaklı performans izlemesi; yavaş rotalar şablon kopyalarına dönüşerek zaman aşımına uğrayabileceğinden oluşturma zamanı önemlidir.
- Bing Webmaster Tools — URL Inspection — bingbot JS’i Googlebot kadar tutarlı oluşturmaz; rotalarınızı Bing tarafında da doğrulayın.
Bir SPA rotasının bağımsız olarak dizine eklenebilir olduğunu kanıtlayın
Doğrudan giriş HTML paritesini test et
Çalıştırılacak test: Temsili rotaları yeni bir oturumda açın ve aynı URL’leri curl ile getirin. Beklenen sonuç: Her URL, önceki uygulama durumu olmadan kendi birincil içeriğini ve head sinyallerini döndürür. Başarısızlık yorumu: Rota istemci gezinmesine veya saklanan duruma bağlıdır. İzleme penceresi: Hemen. Geri alma tetikleyicisi: Bir rota yalnızca ana sayfadan girildikten sonra çalışır.
Hata durumu yönetimini test et
Çalıştırılacak test: Bilinen-geçersiz bir rotayı doğrudan isteyin ve hem durumu hem oluşturulmuş yönergeleri inceleyin. Beklenen sonuç: Gerçek bir hata durumu veya oluşturulmuş noindex/hata yanıtına yönlendirme şeklindeki belgelenmiş geri dönüş. Başarısızlık yorumu: SPA soft-not-found durumları üretiyor. İzleme penceresi: Hemen. Geri alma tetikleyicisi: Geçersiz yollar dizine eklenebilir 200 sayfalar olarak yayına girerse.
Meta veri yalıtımını test et
Çalıştırılacak test: En az üç rotada ham ve oluşturulmuş title, robots etiketi ve canonical’ı karşılaştırın. Beklenen sonuç: Her rota amaçlanan ve kendi içinde tutarlı tek bir kümeye sahip. Başarısızlık yorumu: Paylaşılan kabuk rotalar arasında meta veri sızdırıyor veya JS bunu çok geç üzerine yazıyor. İzleme penceresi: Yerelde hemen, URL Inspection’da yeniden taramadan sonra. Geri alma tetikleyicisi: Herhangi bir rota başka bir rotanın canonical’ını veya kısıtlayıcı bir kabuk yönergesini devralıyor.
Kendinizi sınayın: SPA SEO
Tek sayfalı uygulamaları taranabilir ve dizine eklenebilir kılma üzerine beş kısa soru. Her biri için bir yanıt seçin, sonra kontrol edin.
Zaman ayırmaya değer kaynaklar
İlgili yazılarım
- JavaScript SEO Issues & Best Practices — bu makalenin üzerine kurulduğu “URL’lerde parça kullanmayın” ve app-shell/duplicate-content bölümleri dahil tam JS-SEO rehberim.
- The Beginner’s Guide to Technical SEO — oluşturma ve taramanın büyük resimdeki yeri.
Konuşmalarım
- Arama nasıl çalışır (SlideShare) — SPA’nin hayatta kalması gereken tarama, oluşturma, dizine ekleme ve sıralama hattını anlattığım sunum. (Sürekli kullandığım uyarı geçerlidir: “Bu, sistemleri anlayışım… 100% eksiksiz veya doğru olmayabilir.”)
Sektörden
- Google — Understand the JavaScript SEO basics — app-shell modeli ve “parçalar yerine History API kullanın”.
- Google — Fix Search-related JavaScript problems — SPA soft-404 bölümü ve History API önerisi.
- Google — Deprecating our AJAX crawling scheme (2015) — hash-bang taramasının resmî sona ermesi.
- Bing — bingbot Series: JavaScript, Dynamic Rendering, and Cloaking. Oh My! — bingbot’un JS oluşturma tutumu.
- It’s Not Cloaking To Use A Pre-Render Service For Blank HTML Pages For SPA Development (Search Engine Roundtable, 2015) — Gary Illyes’in SPA’ları önceden oluşturmanın cloaking olmadığına ilişkin haber. (SER’in 2015 tarihli Illyes özeti — ikincil.)
- SEO for Single Page Applications (Nuxt SEO) — aynı sorunların framework tarafındaki anlatımı.
- How To Optimize Single Page Applications For SEO (DebugBear) — SPA dizine eklenebilirliğinin oluşturma/performans açısı.
- SPA (glossary) (MDN) — tek sayfalı uygulama modelinin tarafsız tanımı.
Videolar
- Google Search Central (YouTube) — Martin Splitt’in JavaScript SEO serisi, burada ele alınan oluşturma, History API ve SPA arıza biçimlerini kapsar. Channel
Değişiklik günlüğü
8 Ağu 2026 tarihinde güncellendi.
Editoryal özet ve kaydedilen değişiklik ayrıntıları.Değişiklik ayrıntıları
-
Ayrıntılı değişiklik notları şu anda İngilizce olarak mevcut.
Tam karşılaştırma kullanılamıyor — bu sürüm için önceki anlık görüntü arşivlenmemiş.
18 Tem 2026 tarihinde güncellendi.
Editoryal özet ve kaydedilen değişiklik ayrıntıları.Değişiklik ayrıntıları
-
Ayrıntılı değişiklik notları şu anda İngilizce olarak mevcut.
-
Ayrıntılı değişiklik notları şu anda İngilizce olarak mevcut.
-
Ayrıntılı değişiklik notları şu anda İngilizce olarak mevcut.
-
Ayrıntılı değişiklik notları şu anda İngilizce olarak mevcut.
-
Ayrıntılı değişiklik notları şu anda İngilizce olarak mevcut.
Tam karşılaştırma kullanılamıyor — bu sürüm için önceki anlık görüntü arşivlenmemiş.