Tüm yazılar
Araçlar 6 dk okuma

Kırık link ve yönlendirme zinciri denetimi: sitemap'ten otomatik tarama

Sitemap'ten URL'leri çıkarıp durum kodlarını ve yönlendirme zincirlerini toplu kontrol eden Python betiği; döngü tespiti ve kırık hedefi bulan iç link taraması.

#seo #python #internal-linking

Kırık link ve yönlendirme zinciri denetimi çoğu zaman "site haritasına bak, hepsi 200 dönüyor mu" sorusuna indirgeniyor; oysa asıl risk tek bir 404'te değil, art arda gelen 301'lerin oluşturduğu zincirde ve kimsenin fark etmediği döngülerde. Bir site birkaç yüz sayfayı geçtiğinde elle link kontrolü sürdürülebilir olmaktan çıkıyor; bir yönlendirme kuralı eklerken eskisini silmeyi unutmak ya da bir sayfayı taşırken eski URL'yi yeni hedefe değil ara bir adrese bağlamak, zamanla üç dört adımlık zincirlere dönüşüyor.

Google bu zincirleri takip ediyor ama sınırsız değil; her sıçrama tarama bütçesinden bir pay harcıyor ve bağlantı değerinin bir kısmı yolda kayboluyor. Ziyaretçi tarafında ise her yönlendirme ekstra bir HTTP isteği, yani gecikme demek. Bu yazıda sitemap'ten URL listesini çıkarıp durum kodlarını ve yönlendirme zincirini toplu kontrol eden bir Python betiğini, döngü tespiti mantığını, hangi iç linkin hangi kırık hedefe gittiğini bulan ikinci bir taramayı ve düzeltmeleri hangi sırayla yaptığımı anlatacağım.

Buradaki betikler kendi sitemin yayın öncesi kontrol adımına girdi; her yeni yazıdan sonra elle değil, otomatik koşan bir kontrol. Sitemap'i zaten yayın betiği ürettiği için buradaki tarama onun doğal bir uzantısı; ayrı bir araç kurmaya gerek kalmadı.

Neden tek 404'ten çok yönlendirme zinciri önemli

Denetlediğim sitelerde üç ayrı sorun birbirine karışıyor; ayrı ayrı ele almak gerekiyor çünkü hem etkileri hem düzeltme yöntemleri farklı.

  • Kırık iç link: sitenin kendi içindeki bir sayfa, artık var olmayan bir URL'ye bağlanıyor. Ziyaretçi 404'e düşüyor, Google ise o linki takip etmeye devam ediyor ve tarama bütçesini boşa harcıyor.
  • Yönlendirme zinciri: A, B'ye; B, C'ye yönleniyor. Tarayıcı sonunda C'ye ulaşıyor ama her sıçrama bir ağ gidiş-dönüşü demek; mobilde bu gecikme LCP'yi doğrudan etkiliyor, bunu Core Web Vitals denetiminde ayrıntılı anlattım. Google iki üç sıçramayı sorunsuz takip ediyor ama zincir uzadıkça bağlantı değerinin ne kadarının hedefe ulaştığı belirsizleşiyor.
  • Yönlendirme döngüsü: zincirin bir noktası kendine ya da zincirin başına geri dönüyor. Tarayıcı bir süre sonra hatayla duruyor, Googlebot ise o URL'yi tamamen tarama dışı bırakıyor. Genelde iki ayrı yönlendirme kuralının farkında olmadan birbirini işaret etmesinden çıkıyor.

Üçü de sitenin büyüklüğüyle orantılı artıyor; on sayfalık bir sitede elle takip edilebilir, yüzlerce sayfada değil. Aşağıdaki betik bu üçünü tek taramada ayırt ediyor.

Sitemap'ten URL listesini çıkarıp toplu kontrol

Betik önce sitemap.xml'i okuyup içindeki tüm <loc> etiketlerini çıkarıyor; sitemap zaten yayın betiğinin ürettiği tam URL listesi olduğu için ayrıca bir tarayıcı yazmama gerek kalmıyor. Her URL için HEAD isteği atıyorum, gövdeyi indirmiyorum; yüzlerce sayfada bant genişliğinden tasarruf ediyor. Yönlendirme aldığımda Location başlığını okuyup zincire ekliyorum ve aynı adres tekrar çıkarsa döngü olarak işaretleyip duruyorum; sınırsız döngüye girmemek için en fazla on sıçrama izin veriyorum, bu sınıra ulaşmak da başlı başına bir sorun sinyali.

import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET

SITEMAP = "https://ekremdegirmenci.com/sitemap.xml"
MAX_ZINCIR = 10   # bu kadar sicramadan sonra pes edip dongu varsayiyoruz
NS = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}

def sitemap_urlleri(adres):
    with urllib.request.urlopen(adres, timeout=30) as yanit:
        agac = ET.fromstring(yanit.read())
    return [el.text for el in agac.findall(".//sm:loc", NS)]

def zinciri_takip_et(baslangic):
    zincir = [baslangic]
    hedef = baslangic
    for _ in range(MAX_ZINCIR):
        istek = urllib.request.Request(
            hedef, method="HEAD", headers={"User-Agent": "link-denetim/1.0"}
        )
        try:
            with urllib.request.urlopen(istek, timeout=15) as yanit:
                return zincir, yanit.status   # 200 ile bitti
        except urllib.error.HTTPError as hata:
            if hata.code not in (301, 302, 307, 308):
                return zincir, hata.code   # 404, 410, 5xx: son durum
            konum = hata.headers.get("Location")
            if not konum:
                return zincir, hata.code
            hedef = urllib.parse.urljoin(hedef, konum)
            if hedef in zincir:
                return zincir + [hedef], "DONGU"
            zincir.append(hedef)
        except urllib.error.URLError as hata:
            return zincir, f"BAGLANTI_HATASI:{hata.reason}"
    return zincir, "ZINCIR_UZUN"

if __name__ == "__main__":
    for url in sitemap_urlleri(SITEMAP):
        zincir, sonuc = zinciri_takip_et(url)
        if sonuc != 200:
            print(f"SORUN   {url}  sonuc={sonuc}  atlama={len(zincir) - 1}  {zincir}")
        elif len(zincir) > 1:
            print(f"ZINCIR  {url}  {len(zincir) - 1} atlama  {zincir}")

Çıktıda iki satır türü var: SORUN 200 dışında biten her şeyi (404, 410, 5xx, ağ hatası) listeliyor; ZINCIR sonunda 200 dönen ama en az bir sıçrama yapan URL'leri gösteriyor. İkincisi hata değil ama düzeltilebilir: hedefin doğrudan olması gerekiyordu.

Zincir derinliği ve döngü tespiti

Döngü tespiti basit bir kural: bir sonraki hedef, o ana kadar gezilen zincirde zaten varsa döngüdür, daha ileri gitmeye gerek yok. Bunu bir küme yerine liste ile tutuyorum çünkü hem gezilen sırayı hem tekrar eden adresi raporda görmek istiyorum; sıra bilgisi olmadan hangi kuralın hangi kuralı tetiklediğini anlamak zorlaşıyor.

Zincir uzunluğu için kendi eşiğim bir sıçrama: sonuç 200 olsa bile ikinci bir yönlendirmeye takılan her URL'yi düzeltme listesine alıyorum. Tek sıçramayı (örneğin http'tan https'e ya da www'suz sürüme) sorun saymıyorum; bunlar genelde sunucu seviyesinde kalıcı kurallar ve her URL'de değişmeyecek şekilde sabit. Asıl kazanç, art arda iki ya da üç kuralın üst üste bindiği ve muhtemelen kimsenin fark etmeden biriktirdiği zincirleri yakalamak; bunlar genelde bir taşıma ya da yeniden yapılandırma sırasında oluşup öyle kalıyor.

Yönlendirme döngülerinin en sık sebebi aynıdır: bir sayfa yeni bir URL'ye taşınırken eski kural silinmeden bırakılır, yeni kural da bir noktada eskiye geri işaret eder. Betik bunu ilk taramada satırla gösterince düzeltmesi beş dakika sürüyor; taramasız durumda ise yalnızca Search Console'un "yönlendirme hatası" raporunda haftalar sonra ortaya çıkıyor.

İlk betik hangi URL'nin bozuk olduğunu söylüyor ama hangi sayfadan link verildiğini söylemiyor; bunu düzeltmek için kaynak dosyayı elle aramak gerekiyor. İkinci betik bu boşluğu kapatıyor: blog/articles/ altındaki her HTML dosyasını tarıyor, / ile başlayan iç linkleri regex ile çıkarıyor ve ilk taramanın bulduğu kırık hedeflerle eşleştiriyor. Dış linklere bakmıyorum; onlar ayrı bir konu ve genelde hedef sitenin kendi sorumluluğunda.

import re
from pathlib import Path

KOK = "https://ekremdegirmenci.com"
HREF = re.compile(r'href="(/[^"#]*)"')   # / ile baslayan ic linkler, capa (#) haric

def ic_linkleri_bul(dosya):
    metin = dosya.read_text(encoding="utf-8")
    return set(HREF.findall(metin))

def rapor(kirik_hedefler):
    for dosya in sorted(Path("blog/articles").glob("*.html")):
        for yol in ic_linkleri_bul(dosya):
            tam = KOK + yol
            if tam in kirik_hedefler:
                print(f"{dosya.name}  ->  {yol}")

if __name__ == "__main__":
    # onceki taramanin SORUN satirlarindan ikinci sutunu (URL) topla
    kirik_hedefler = {
        satir.split()[1] for satir in Path("sorunlar.txt").read_text().splitlines()
    }
    rapor(kirik_hedefler)

Çıktı, dosya adı ile kırık hedefin yan yana durduğu bir liste; her satır tek bir düzeltme demek. Bu ikinci taramayı yaptığım için artık "şu URL kırık" bilgisiyle baş başa kalmıyorum, doğrudan hangi yazıyı açıp hangi <a href>'i değiştireceğimi biliyorum. Aynı mantığı Googlebot tarama bütçesi analizinde kullandığım log eşleştirmesiyle karşılaştırıyorum: orada hangi URL'nin çok taranıp az değer ürettiğini buluyordum, burada hangi sayfanın kendi kendine zarar verdiğini buluyorum. İkisi de aynı disiplin: ham bir uyarı listesini kaynağına bağlamadan düzeltme adımı başlamıyor.

Düzeltme sırası ve yayın öncesi kapı

Bulguları önceliklendirmeden düzeltmeye başlamak, en görünür ama en az değerli sorunla uğraşarak zaman kaybetmek anlamına geliyor. Benim uyguladığım sıra şu:

  1. Önce iç link kaynaklı 404'leri düzelt: bunlar tamamen kendi kontrolümde, ikinci betiğin çıktısındaki her satır doğrudan bir düzenleme.
  2. Sonra iç linklerdeki zincirleri kısalt: linki ara adrese değil doğrudan son hedefe yönlendir. Yönlendirme kuralını silmiyorum, dışarıdan gelen eski bağlantılar için kalıyor; yalnızca kendi sayfalarımın birbirine dolaysız bağlanmasını sağlıyorum.
  3. Döngüleri en yüksek öncelikle ele alıyorum; bunlar sayfayı tamamen taranamaz hale getiriyor ve genelde tek bir yanlış kuraldan kaynaklanıyor.
  4. Sitemap'te olup 404 dönen URL'leri sitemap'ten çıkarıyorum; sitemap'te kırık URL listelemek Google'a doğrudan güvenilirlik kaybettiriyor.

Bu taramayı yayın betiğinin doğrulama adımının hemen ardına eklemek iyi bir düzendir: yeni bir yazı sitemap'e girer girmez aynı gün taranır, sorun varsa haftalar sonra değil ertesi gün fark edilir. Haftalık ayrı bir koşum da tutuyorum çünkü kırık hedef yalnızca yeni yazı eklerken değil, eski bir yazıyı düzenlerken ya da bir kategori sayfasını yeniden adlandırırken de ortaya çıkabiliyor.

Özetle: kırık iç link, yönlendirme zinciri ve döngü üç ayrı sorun ve üçü de site büyüdükçe elle takip edilemez hale geliyor; sitemap'ten URL listesini çekip zinciri adım adım izleyen bir Python betiği bunların hepsini tek taramada ayırt ediyor, ikinci bir tarama da hangi sayfanın kaynağı olduğunu gösteriyor. Kendi siteniz için böyle bir denetim düzeni kurmak isterseniz veri ve otomasyon danışmanlığı işimi anlattığım ana sayfadaki /#contact bölümünden bana yazabilirsiniz.