Çokdilli SEO: 87 Sayfalık Hreflang Kazası ve Canonical Stratejimiz

87 docs sayfamızın hepsi aynı iki hreflang adresini ilan ediyordu; Google kümeyi yok saydı. İstek yolundan üretilen çift, TR sluglar, x-default ve 301'ler.

Çokdilli SEO: 87 Sayfalık Hreflang Kazası ve Canonical Stratejimiz

87 docs sayfamızın hepsi aynı iki hreflang adresini ilan ediyordu; Google kümeyi yok saydı. İstek yolundan üretilen çift, TR sluglar, x-default ve 301'ler.

87 sayfa, tek bir elle yazılmış hreflang çifti

jekcms.com her sayfayı tek kod tabanından hem İngilizce hem Türkçe servis ediyor. İngilizce öneksiz adreslerde yaşıyor, Türkçe /tr/ altında ve yerelleştirilmiş sluglarla: /pricing/tr/fiyatlandirma, /about/tr/hakkimizda. Yalnızca dokümantasyon bölümü 87 sayfa; her biri iki dilde.

Docs bölümünün render edilmiş <head> çıktısını toplu halde grep'lerken — filo çapındaki SEO denetimini doğuran alışkanlığın aynısı — her docs sayfasında şunu buldum:

<link rel="alternate" hreflang="en" href="https://jekcms.com/docs">
<link rel="alternate" hreflang="tr" href="https://jekcms.com/tr/dokumantasyon">

87 sayfanın 87'sinde. Aynı iki adres. Etiketler docs header parçasına, bölüm henüz tek bir karşılama sayfasından ibaretken elle yazılmış; sonradan eklenen her sayfa da bunları olduğu gibi miras almış.

Bunun Google'a ne söylediğine bakın: /docs/automation/webhooks gibi bir sayfa, Türkçe karşılığının dokümantasyon giriş sayfası olduğunu iddia ediyordu — yanlış — ve daha kötüsü, İngilizce karşılığının /docs olduğunu. Kendisi değil. Başka bir sayfa.

Google kümeyi neden komple çöpe attı?

hreflang küme mantığıyla çalışır. Dil grubundaki her URL, kendisi dahil bütün alternatifleri listelemek zorundadır ve listelenen her alternatif, eşleşen bir setle geri işaret etmelidir. Kendine referans ya da dönüş bağlantısı eksikse açıklama doğrulamadan geçmez — kısmen değil; Google o sayfanın sinyalini tamamen atar.

Bizim docs bölümünde geçerli açıklamaya sahip tam iki sayfa vardı: /docs ve /tr/dokumantasyon'un kendileri — kazara, çünkü elle yazılmış çift tesadüfen onları tarif ediyordu. Kalan 85 sayfa çelişki yayınlıyordu. Net sonuç: 87 sayfa dolusu hreflang işaretlemesi, kullanılabilir sinyal sıfıra yakın. Türkçe docs sayfaları İngilizce sorgularda çıkmaya devam etti, tersi de öyle — hreflang'in önlemek için var olduğu problemin ta kendisi.

İşaretleme view-source'ta eksiksiz görünüyordu. Bu tür hataların bu kadar uzun hayatta kalmasının sebebi tam olarak bu.

Çözüm: çifti istek yolundan üret

Herhangi bir sayfanın doğru hreflang çifti, o an servis edilen URL'nin bir fonksiyonudur. O halde şablona yazmak yerine tam orada, istek anında hesaplanmalı:

function hreflang_pair(): array {
    $path = strtok($_SERVER['REQUEST_URI'], '?');   // sorgu dizesi yok
    $base = rtrim(SITE_URL, '/');

    if (str_starts_with($path, '/tr/')) {
        $tr = $path;
        $en = tr_to_en_path($path);   // slug haritası, aşağıda
    } else {
        $en = $path;
        $tr = en_to_tr_path($path);
    }

    return ['en' => $base . $en, 'tr' => $base . $tr];
}

$pair = hreflang_pair();
foreach (['en', 'tr'] as $lang) {
    echo '<link rel="alternate" hreflang="' . $lang . '" href="'
        . htmlspecialchars($pair[$lang]) . '">' . "\n";
}
echo '<link rel="alternate" hreflang="x-default" href="'
    . htmlspecialchars($pair['en']) . '">' . "\n";

Üretim ortamında önemli olan ayrıntılar:

  • Sorgu dizesi her şeyden önce atılıyor. Parametrelerin hreflang'te de canonical'da da yeri yok.
  • Kendine referans bedavaya geliyor: mevcut sayfanın kendi yolu her zaman çiftin bir tarafı.
  • Karşılıklılık da bedavaya geliyor. Türkçe sayfa aynı çifti haritanın kendi tarafından hesaplıyor; iki set birbirinden ayrı düşemiyor.

Asıl ders son madde. Çift yönlülük elle koruduğunuz bir şey değildir; mimarinin ya garanti ettiği ya da etmediği bir şeydir.

Yerelleştirilmiş Türkçe sluglar eşleme tablosuna değer

Türkçe URL'leri önek ekleyerek de kurabilirdik: /tr/about, /tr/pricing. Alternatifi hesaplamak tek satırlık string işi olurdu. Yerelleştirmeyi seçtik: /tr/hakkimizda, /tr/fiyatlandirma, /tr/dokumantasyon.

İki sebep. Türkçe kelimelerle arama yapan kullanıcı URL'de de Türkçe kelime görmeyi hak ediyor — slug hem bir alaka sinyali hem de arama sonucundaki snippet'te bir güven sinyali. Ve Türkçe içeriğin önündeki İngilizce yol parçası, sonradan akla gelmiş gibi durur; çünkü öyledir.

Bedeli şu: en_to_tr_path() string manipülasyonu olamıyor. Blog yazıları için bu bir sorgu — her yazı satırında slug_en ve slug_tr sütunları var, yani harita veritabanının kendisi. Statik ve docs sayfaları içinse router'ın yanında duran açık bir dizi. Yazması sıkıcı; ama mesele de o sıkıcılık: açık bir haritanın sessiz bozulma modu yoktur. Bir sayfa haritada eksikse yardımcı fonksiyon null döner, o sayfa için hreflang bloğunu komple atlarız ve bir log satırı bana kaydı eklememi söyler. Yanlış-ama-makul görünen üretilmiş bir URL çok daha kötü olurdu — az önce temizlediğimiz hata sınıfının ta kendisi.

İki dil ağacını editoryal olarak nasıl senkron tuttuğumuz ayrı bir yazının konusu; burada mesele URL katmanı.

x-default İngilizce'ye gidiyor — varsayılan değil, karar

x-default açıklaması, iki dille de eşleşmeyen kullanıcıya gösterilecek sayfayı adlandırır. Bizimki İngilizce sürüme işaret ediyor: Türkiye dışında bir PHP CMS'in kitlesi ezici çoğunlukla İngilizce okuyor ve öneksiz ağaç zaten yönlendirmemizdeki birincil ağaç.

Birincil pazarınız Türkçeyse tersine çevirin. Yapmamanız gereken, onu hiç koymamak ya da var olmayan bir dil seçim sayfasına doğrultmak. Aynı çiftten hesaplanan, sayfa başına tek ek satır.

?lang= bir render modu değil, yönlendirmedir

jekcms.com'un ilk dil değiştiricisi ?lang=tr parametresini kullanıyor ve hangi URL'deyseniz orada Türkçe içerik render ediyordu. Gerçek /tr/ yolları devreye girince bu parametre bir yük haline geldi: /docs?lang=tr, canonical'ı da hreflang'i de "İngilizce sayfa" diyen bir URL'de Türkçe içerik demek. Bu şekildeki her render, temiz sinyalleri kemiren bir yinelenen-içerik varyantı.

Artık ?lang= taşıyan her istek, yol tabanlı karşılığına 301 alıyor — tek bir bayt çıktı üretilmeden önce; çünkü şablon yazmaya başladıktan sonra denenen yönlendirme, yönlendirme değil bozuk sayfa üretir:

if (isset($_GET['lang'])) {
    $path   = strtok($_SERVER['REQUEST_URI'], '?');
    $target = $_GET['lang'] === 'tr' ? en_to_tr_path($path) : tr_to_en_path($path);
    header('Location: ' . ($target ?? $path), true, 301);
    exit;
}

302 değil, 301. Parametreli URL'lerin arkasında yılların geçmişi vardı; kalıcı yönlendirme, taşıdıkları değeri kanonik yollara aktarır. Birkaç tarama döngüsü içinde parametreli varyantlar indeksten düştü.

Canonical stratejisi: her dilde kendine referans, her zaman

Her sayfa tam bir canonical basar ve o canonical kendisine işaret eder. jekcms bunun için output_canonical_tag() fonksiyonunu hazır veriyor ve varsayılan davranışı doğru olan. Türkçe sayfanın canonical'ı Türkçe URL'dir. Nokta.

Cazip hata, diller arası canonical: Türkçe sayfanın canonical'ını İngilizce "orijinale" doğrultmak. Bu, Google'a iki çelişkili mesaj gönderir — canonical "ben kopyayım, öbürünü indeksle" der, hreflang "biz farklı kitleler için eşit alternatifleriz" der. Google çelişkiyi canonical'a güvenerek çözer ve Türkçe sayfanız sessizce indeksten çıkar.

Bir kısıt daha: canonical URL ile hreflang'teki kendine-referans URL'si bayt bayt aynı olmalı. Aynı şema, aynı host, aynı sondaki-eğik-çizgi kuralı. İkisini de aynı yol yardımcısından geçiriyoruz ki birbirlerinden sapamasınlar.

Beslemeler de dile göre ayrıldı

RSS beslemesi eskiden iki dili iç içe veriyordu. Her abone, öğelerin yarısını okumadığı bir dilde alıyordu ve beslemenin <language> öğesi girdilerin yarısı için yanlıştı. Beslemeler tarayıcılar için aynı zamanda bir keşif girdisi; karışık besleme, hreflang çalışmasının yeni temizlediği sinyali yeniden bulandırıyor.

Çözüm URL şemasını aynalıyor: öneksiz ağaçta İngilizce besleme, /tr/ altında Türkçe besleme; her biri kendi dilini ilan ediyor:

<language>en-us</language>   <!-- İngilizce besleme -->
<language>tr-tr</language>   <!-- Türkçe besleme -->

Her sayfanın head'i yalnızca kendi dilinin beslemesini bağlıyor. Türkçe sayfadan abone olan okur Türkçe besleme alıyor. Geriye dönüp bakınca bariz.

Kendi sitenizi on dakikada kontrol edin

Araç tartışmalarını geçin; bu hata sınıfının tamamını iki komut buluyor:

# derin bir sayfa KENDİSİNE referans veriyor mu?
curl -s https://ornek.com/docs/derin-bir-sayfa | grep -i hreflang

# ilan ettiği alternatif geri işaret ediyor mu?
curl -s https://ornek.com/tr/derin-sayfa | grep -i hreflang

Bunu ana sayfada değil, birkaç derin sayfada çalıştırın. Ana sayfa, kazara doğru olma ihtimali en yüksek sayfadır — bizimki öyleydi. Bir bölümdeki her sayfa birebir aynı hreflang adreslerini basıyorsa, bizim hatamız sizde de var demektir.

Sonrası sabır işi. Düzeltmeden sonra Google'ın docs kümesini baştan işlemesi haftalar aldı ve Search Console'da bunu izleyecek eski Uluslararası Hedefleme raporu artık yok — dürüst doğrulama yolu, tek tek sayfalarda URL Denetimi. İlk gün doğrulayabileceğiniz kısım işaretlemenin kendisi: her URL ya kendini ve gerçek alternatifini ilan ediyordur ya da etmiyordur. Bu kod tabanında 14 site işlettikten sonra hreflang özetim kısa: zor değil; sadece kestirmeleri affetmiyor.

Yazar

Celil Uyanıkoğlu

25 yılı aşkın süredir bilgi işlem sektöründe çalışan bir bilgisayar mühendisi. jekcms'i geliştiriyor ve kendi yayın ağındaki sitelerin tamamını jekcms üzerinde çalıştırıyor — burada yayımlanan her rehber önce o canlı kurulumlarda denenir.

Tüm yazılarını gör →

Hemen Sipariş Verin

Tek seferlik ödeme, ömür boyu erişim. Kurulum 30 dakika.

Fiyatlara Bak
  • 30 dakikada kurulum ve yayın
  • 13 profesyonel tema
  • AVIF/WebP görsel optimizasyonu
  • Otomatik SEO — Sitemap, Schema.org
  • ZeroTrack çerezsiz analitik

Yeniliklerden ilk sen haberdar ol

Yeni özellikler, sürüm notları ve CMS rehberleri — ayda birkaç e-posta, spam yok.