React ve Vue sitelerinde SEO: SSR, SSG ve prerender

React ya da Vue ile yapılmış bir sitenin SEO sorunları çoğu zaman framework'ten değil, render stratejisinden çıkıyor. Aynı React bileşenleri, tarayıcıda boş bir <div id="root"> içine de basılabilir, sunucuda tam HTML olarak da üretilebilir. Google açısından bu ikisi arasında büyük fark var.

Doğru stratejiyi seçmek için önce seçeneklerin ne yaptığını netleştirmek gerekiyor. Sonra sitenin türüne bakıp karar vermek kolaylaşıyor.

Beş render stratejisi

CSR (Client-Side Rendering)

Sunucu neredeyse boş bir HTML kabuğu ve JavaScript paketleri gönderiyor. İçerik tarayıcıda, JavaScript çalıştıktan sonra oluşuyor. Create React App ya da varsayılan Vite kurulumlarının ürettiği şey bu.

Google CSR sayfaları render edebiliyor, ama içerik sadece render aşamasından sonra görünür oluyor. Render kuyruğu, engellenen kaynaklar, zincirleme API çağrıları ve SPA'larda soft 404 gibi riskler burada toplanıyor. Bu süreci Googlebot JavaScript'i nasıl işliyor yazısında ayrıntılı anlattım. Ayrıca Google dışındaki arama motorları ve birçok yapay zekâ tarayıcısı JavaScript'i hiç çalıştırmayabiliyor.

SSR (Server-Side Rendering)

Her istekte sunucu sayfayı tam HTML olarak üretiyor, tarayıcı gelen HTML'i gösteriyor ve ardından JavaScript "hydration" ile sayfayı etkileşimli hâle getiriyor. İçerik ilk HTML'de olduğu için tarayıcı ve bot aynı şeyi görüyor. Doğru durum kodunu (404, 301) sunucudan döndürmek de mümkün.

Maliyeti sunucu tarafında: her istek işlem gücü gerektiriyor ve iyi bir önbellek stratejisi olmadan TTFB yükselebiliyor.

SSG (Static Site Generation)

Sayfalar derleme (build) sırasında bir kez HTML olarak üretiliyor ve statik dosya olarak sunuluyor. En hızlı ve en ucuz seçenek. Sınırı şu: içerik değiştiğinde yeniden derleme gerekiyor. Birkaç yüz sayfalık bir blog için sorun değil, yüz binlerce ürünlü bir mağaza için pratik değil.

ISR (Incremental Static Regeneration)

SSG ile SSR arasında bir yol. Sayfa statik olarak sunuluyor ama belirli bir süre sonra ya da tetiklendiğinde arka planda yeniden üretiliyor. Next.js'te revalidate, Nuxt'ta routeRules içindeki ISR ve SWR ayarları bu işi görüyor. Büyük ve içeriği sık değişen siteler için iyi bir denge.

Prerendering

Mevcut bir CSR uygulamasını değiştirmeden, sayfaları headless bir tarayıcıyla önceden render edip statik HTML olarak kaydetmek. Az sayıda ve sabit sayfası olan bir SPA için geçici bir çözüm olabilir. Ama sayfa sayısı arttıkça ve içerik sık değiştikçe yönetmesi zorlaşıyor.

Dinamik render artık önerilmiyor

Bir dönem yaygın olan yöntem, user agent'a bakıp botlara prerender edilmiş HTML, kullanıcılara CSR sürümü göndermekti. Google bunu artık kalıcı bir çözüm olarak önermiyor ve dinamik render belgesinde bir geçici çözüm (workaround) olarak tanımlıyor; önerilen yol SSR, SSG ya da hydration ile birlikte statik render.

Dinamik render'ın pratik sorunları da var: iki ayrı sürümün bakımı, botlara giden sürümün bozulduğunu geç fark etmek ve bot tespiti hataları. Mevcut bir kurulumda çalışıyorsa hemen kaldırmanız gerekmiyor, ama yeni bir projede bu yola girmezdim.

Framework'ler

  • Next.js (React): SSR, SSG ve ISR'ı sayfa bazında seçmeye izin veriyor. App Router'da React Server Components sayesinde sadece etkileşimli bileşenlerin JavaScript'i tarayıcıya gidiyor. Meta verileri generateMetadata ile yönetiliyor.
  • Nuxt (Vue): Varsayılan olarak SSR. routeRules ile rota bazında prerender, ISR ya da sadece istemci tarafı render seçilebiliyor. Başlık ve meta etiketleri için useHead ve useSeoMeta var.
  • Astro: İçerik odaklı siteler için tasarlanmış. Varsayılan olarak sıfır JavaScript ile statik HTML üretiyor; etkileşim gereken bileşenler "ada" (island) olarak ayrı ayrı hydrate ediliyor. React, Vue ya da Svelte bileşenlerini içinde kullanabiliyorsunuz.
  • SvelteKit: Varsayılan olarak SSR; export const prerender = true ile sayfa bazında statik üretime geçilebiliyor.

Rota başına head yönetimi

Hangi stratejiyi seçerseniz seçin, her rotanın kendi title, meta description, canonical ve gerekirse robots değerleri ilk HTML'de olmalı. CSR uygulamalarında sık gördüğüm sorun, bütün sayfaların index.html'deki aynı title ile başlaması ve doğru değerlerin sadece JavaScript'ten sonra gelmesi.

Next.js App Router'da bir blog yazısı sayfası şöyle görünebilir:

app/blog/[slug]/page.js
import { notFound } from 'next/navigation';
import { getPost } from '@/lib/posts';

// Sayfa en fazla saatte bir arka planda yeniden üretilir (ISR)
export const revalidate = 3600;

// Her rota için ayrı title, description ve canonical
export async function generateMetadata({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  if (!post) return {};
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `https://example.com/blog/${slug}` },
  };
}

export default async function Page({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  // Yazı yoksa Next.js'in 404 sayfasını göster
  if (!post) notFound();
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

Burada getPost kendi veri katmanınız. notFound() çağrısından sonra durum kodunu curl -I ile kontrol etmeyi unutmayın; streaming kullanılan kurulumlarda davranış farklı olabiliyor ve benim kuralım, framework'e değil test sonucuna güvenmek.

Hydration maliyeti ve INP

SSR ve SSG içeriği ilk HTML'e taşıyor, ama JavaScript maliyetini ortadan kaldırmıyor. Tarayıcı HTML'i gösterdikten sonra bütün bileşen ağacını hydrate etmek için JavaScript'i indirip çalıştırıyor. Bu sırada ana iş parçacığı meşgul oluyor ve kullanıcının tıklamaları bekliyor.

Sonuç, sayfanın görünür olduğu ama tepki vermediği bir aralık. Bu doğrudan INP'ye yansıyor; INP'nin iyi sayılması için 75. yüzdelikte 200 ms veya altında olması gerekiyor. Hydration'ı hafifletmenin yolları:

  • Etkileşim gerektirmeyen bileşenleri sunucu bileşeni (Server Component) ya da statik HTML olarak bırakmak.
  • Astro adaları gibi kısmi hydration kullanmak.
  • Ağır üçüncü taraf script'leri ve widget'ları ertelemek.
  • Büyük bileşenleri kod bölme (code splitting) ile ayırmak.

INP tarafındaki ayrıntılar için INP'yi düzeltmek yazısına bakabilirsiniz.

Hangi site için hangisi?

Site türüÖnerimNeden
Blog, doküman, kurumsal siteSSG (Astro, Next.js, Nuxt)İçerik seyrek değişiyor, en hızlı ve en ucuz çözüm
Haber sitesiSSR veya kısa süreli ISRİçerik dakikalar içinde güncel olmalı
E-ticaret (kategori, ürün)ISR veya önbellekli SSRÇok sayıda sayfa, değişen stok ve fiyat
E-ticaret (sepet, hesap)CSRDizine girmesi gerekmiyor
Dashboard, yönetim paneliCSRGiriş arkasında, SEO ihtiyacı yok
Mevcut büyük CSR uygulamasıÖnce kritik sayfalarda SSR/SSG'ye geçişTamamen yeniden yazmak yerine kademeli
Az sayfalı, değişmeyen SPABuild sırasında prerenderEn az değişiklikle ilk HTML'i doldurmak

Aynı sitede farklı stratejiler kullanmak normal. Modern framework'ler bunu rota bazında seçmenize izin veriyor: blog yazıları SSG, ürün sayfaları ISR, hesap sayfaları CSR olabilir.

Nasıl doğrularım?

Strateji ne olursa olsun, son kontrol aynı: sayfanın ham HTML'inde içerik, title, canonical ve linkler var mı? curl ile alınan HTML'i render edilmiş DOM ile karşılaştırmak için render farkını test etmek yazısındaki yöntemi kullanıyorum. İki sürüm arasında önemli bir fark yoksa, render stratejiniz SEO açısından işini yapıyor demektir.

Özetle

Dizine girmesi gereken sayfalar için içerik ilk HTML'de olmalı; bu da pratikte SSR, SSG ya da ISR demek. CSR'ı giriş arkasındaki sayfalara bırakın, dinamik render'ı yeni projelerde kullanmayın ve SSR'ın hydration maliyetini INP üzerinden takip edin. Seçim yaparken framework'ün popülerliğine değil, sitenizin kaç sayfası olduğuna ve içeriğin ne sıklıkla değiştiğine bakın.

Comments / questions

If you have a question about any topic or wish to contact me: Telegram or send an email to [email protected] Thanks!

Related pages