Canonical URL: Duplicate Content'i Yönetmek ve Google'ın Seçtiği Canonical
rel=canonical bir emir değil, sinyaldir: Google redirect, canonical, sitemap ve iç link'lerin tutarlı olduğu URL'yi seçer. ASP.NET Core'da Host header'a güvenmeyen ve gereksiz parametreleri atan bir canonical tag helper yazıyor, PDF için Link header'ı, Angular SSR'da tek canonical'ı ve Google'ın Aralık 2025 JavaScript kuralını gerçek çıktılarla görüyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10 Razor Pages) ve Angular 22.2 ile derlenip denenmiştir; canonical kuralları Google Search Central'ın Kasım 2026 hâli esas alınarak anlatılmıştır.
Önce Tahmin Et
Bookstore'da Dune'un sayfası şu adreslerin hepsinden açılıyor: https://bookstore.example/books/dune, http://bookstore.example/books/dune, https://www.bookstore.example/books/dune, /books/dune?utm_source=newsletter, /books/dune/print. Hepsi 200 dönüyor, içerik aynı. Bir SEO (Search Engine Optimization) danışmanı "sinyalleri toplamak için bütün kitap sayfalarının canonical'ını ana sayfaya verelim" öneriyor.
İki soru. Danışmanın önerisi uygulanırsa Google kitap sayfalarıyla ne yapar? Ve sayfadaki canonical tag'i A adresini gösterirken sitemap B'yi listeliyor ve sitenin iç link'leri C'ye gidiyorsa, Google sonuçlarda hangi URL'yi (Uniform Resource Locator) gösterir?
Serinin ilk yazısında tek ve kalıcı URL hedefini koymuş, önceki yazıda sitemap'e "yalnız canonical URL'leri koy" demiştik. Bu yazıda canonical kavramını açıyoruz: duplicate content nereden gelir, Google canonical'ı neye bakarak seçer, ASP.NET Core'da ve Angular'da canonical link'i nasıl doğru üretirsin.
Duplicate Content Nereden Gelir?
Google, aynı veya çok benzer içeriği taşıyan URL'leri bir küme olarak gruplar ve kümeden birini canonical seçer; sonuçlarda gösterilen ve sinyallerin toplandığı URL odur, diğerleri duplicate olarak daha seyrek crawl edilir. Duplicate'lerin çoğu kötü niyetten değil, altyapıdan gelir:
- Protocol ve host varyantları:
http/https,www/www'suz. - Path varyantları: büyük/küçük harf, sondaki slash,
index.html. - Query parametreleri: takip parametreleri (
utm_source,ref), sıralama, session ID (Identifier). - Aynı içeriğin farklı sunumları: yazdırma sayfası, AMP (Accelerated Mobile Pages) sürümü, eski mobil subdomain'i.
- Dil ve bölge varyantları: aynı İngilizce içeriğin
/en-us/ve/en-gb/altında yayımlanması. - Sayfalama ve filtre kombinasyonları: aynı ürün listesinin farklı sıralarda gösterilmesi.
Önemli bir ayrım: çeviri duplicate değildir. Google dokümanına göre yerelleştirilmiş sürümler yalnız ana içerik çevrilmemişse duplicate sayılır. Türkçe ve İngilizce sayfalar birbirinin canonical'ı olmaz; ikisi de kendi canonical'ıdır ve aralarındaki ilişkiyi hreflang anlatır.
Canonical Sinyalleri: Güçlü, Zayıf ve Tutarlı
rel=canonical bir emir değil, sinyaldir. Google canonical'ı birden çok sinyale bakarak seçer ve dokümanında bunları güçlerine göre sıralıyor:
| Yöntem | Güç | Ne zaman |
|---|---|---|
| Kalıcı redirect (301, 308) | Güçlü | Duplicate URL'nin artık kullanılmaması gerekiyorsa |
rel="canonical" link element'i veya HTTP (HyperText Transfer Protocol) header'ı | Güçlü | Duplicate URL'nin erişilebilir kalması gerekiyorsa (takip parametresi, print sayfası) |
| Sitemap'te yer almak | Zayıf | Büyük sitelerde diğer yöntemlere ek olarak |
Bunların yanında Google iç link'lere, HTTPS (HTTP Secure) tercihine (Google varsayılan olarak HTTPS'i tercih ediyor) ve karşılıklı hreflang kümelerine de bakıyor. Açılıştaki ikinci sorunun cevabı burada: A, B ve C birbiriyle çeliştiğinde Google'ın hangisini seçeceği tahmin edilemez; canonical tag'i tek başına kazanmaz. Doküman net söylüyor: aynı sayfa için farklı tekniklerle farklı canonical'lar belirtme, iç link'lerini canonical URL'ye ver. Sinyallerin hepsi aynı URL'yi gösterdiğinde seçim neredeyse kesinleşir.
Dokümandaki diğer kurallar: canonical mutlak URL olmalı, canonical sayfa kendini canonical göstermeli (self-referencing), canonicalization için robots.txt, Removals aracı veya noindex kullanılmamalı. Danışmanın önerisinin sorunu da buradan anlaşılıyor: Dune'un sayfası ana sayfanın duplicate'i değil; bütün kitap sayfalarını ana sayfaya canonical'lamak, Google'a "bu sayfalar aslında ana sayfanın kopyası" demek. Google içerikler bariz şekilde farklı olduğu için bu sinyali büyük ihtimalle yok sayar; saymazsa da kitap sayfaları sonuçlardan kaybolur. İki sonuç da istenmeyen.
ASP.NET Core'da Canonical Tag Helper
Bookstore'un Razor Pages uygulamasında canonical'ı her sayfada elle yazmak yerine bir tag helper üretiyor. Kurallar: host ve protocol configuration'dan gelir, path küçük harfe çevrilir, query string'ten yalnız içeriği gerçekten değiştiren parametreler sabit bir sırayla kalır, varsayılan değerler atılır.
using System.Web;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Options;
namespace Bookstore.Canonical.TagHelpers;
public sealed class SiteOptions
{
public required string BaseUrl { get; set; } // "https://bookstore.example"
}
[HtmlTargetElement("canonical-link", TagStructure = TagStructure.WithoutEndTag)]
public sealed class CanonicalLinkTagHelper(IHttpContextAccessor accessor, IOptions<SiteOptions> site) : TagHelper
{
// içeriği gerçekten değiştiren query key'leri, sabit sırayla
public string KeepQuery { get; set; } = "";
public override void Process(TagHelperContext context, TagHelperOutput output)
{
HttpRequest request = accessor.HttpContext!.Request;
string path = (request.PathBase + request.Path).Value!.ToLowerInvariant();
List<string> parts = [];
foreach (string key in KeepQuery.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
string? value = request.Query[key].FirstOrDefault();
if (string.IsNullOrEmpty(value) || (key == "page" && value == "1"))
continue; // varsayılanları at: ?page=1 aynı sayfa
parts.Add($"{key}={HttpUtility.UrlEncode(value)}");
}
// base URL configuration'dan gelir, asla Host header'ından değil
string href = site.Value.BaseUrl + path + (parts.Count > 0 ? "?" + string.Join('&', parts) : "");
output.TagName = "link";
output.TagMode = TagMode.SelfClosing;
output.Attributes.SetAttribute("rel", "canonical");
output.Attributes.SetAttribute("href", href);
}
}
Sayfalarda kullanımı tek satır; liste sayfası hangi parametrelerin canonical'a girdiğini kendisi söylüyor:
@page "/books"
<!DOCTYPE html>
<html lang="en">
<head>
<title>Books | Bookstore</title>
<canonical-link keep-query="genre,page" />
</head>
<body><h1>Books</h1></body>
</html>
$ curl -s "http://localhost:20106/books/dune" | grep canonical
<link rel="canonical" href="https://bookstore.example/books/dune" />
$ curl -s "http://localhost:20106/Books/Dune?utm_source=newsletter&ref=home" | grep canonical
<link rel="canonical" href="https://bookstore.example/books/dune" />
$ curl -s "http://localhost:20106/books?genre=scifi&sort=price&page=2&utm_campaign=bf" | grep canonical
<link rel="canonical" href="https://bookstore.example/books?genre=scifi&page=2" />
$ curl -s "http://localhost:20106/books?page=1&genre=scifi" | grep canonical
<link rel="canonical" href="https://bookstore.example/books?genre=scifi" />
$ curl -s "http://localhost:20106/books?sort=title" | grep canonical
<link rel="canonical" href="https://bookstore.example/books" />
Takip parametreli ve büyük harfli istek temiz adrese, beş parametreli liste genre ve page'e, page=1 parametresiz adrese iniyor. Sıralama parametresi her zaman atılıyor, çünkü aynı listeyi farklı sırada göstermek yeni içerik değil. HTML (HyperText Markup Language) içindeki &, attribute içindeki &'in doğru encode edilmiş hâli; tarayıcı ve crawler bunu & olarak okur. Sayfalamanın ikinci sayfası (page=2) kendi canonical'ını taşıyor; ikinci sayfayı birinci sayfaya canonical'lamak, ikinci sayfadaki kitapların keşfini zayıflatan yaygın bir hatadır.
Base URL'nin Host header'ından değil configuration'dan gelmesinin nedenini en kolay naif versiyon gösteriyor. Klasik kestirme şu: canonical, isteğin geldiği URL ne ise o.
@page "/naive/{slug}"
<link rel="canonical" href="@($"{Request.Scheme}://{Request.Host}{Request.Path}{Request.QueryString}")" />
$ curl -s -H "Host: evil.example" "http://localhost:20106/naive/dune?utm_source=x"
<link rel="canonical" href="http://evil.example/naive/dune?utm_source=x" />
$ curl -s -H "Host: evil.example" "http://localhost:20106/books/dune" | grep canonical
<link rel="canonical" href="https://bookstore.example/books/dune" />
Naif versiyon üç hatayı birden yapıyor: takip parametresini canonical'a taşıyor, reverse proxy arkasında Request.Scheme http olduğu için HTTPS sitesinde http canonical üretiyor ve istemcinin gönderdiği Host header'ını olduğu gibi sayfaya yazıyor. Son madde bir cache poisoning riski: araya giren bir cache bu yanıtı saklarsa, başka bir domain'i canonical gösteren sayfa herkese servis edilir. appsettings.json'daki AllowedHosts ayarını * yerine gerçek domain'lerle doldurmak Host filtering'i açar ve bu isteği 400 ile reddeder; yine de URL üreten kodun Host header'ına güvenmemesi ayrı bir savunma katmanı. Reverse proxy arkasında doğru scheme ve host için forwarded header'lar Web Server'lar ve Hosting serisinde.
HTML olmayan dosyalar için canonical, HTTP header'ında Link ile verilir. Bookstore'da aynı katalog PDF'i hem eski /downloads/catalog.pdf hem yeni /uploads/catalog-2026.pdf adresinde duruyor:
app.MapGet("/downloads/catalog.pdf", (HttpContext ctx) =>
{
ctx.Response.Headers.Link = "<https://bookstore.example/uploads/catalog-2026.pdf>; rel=\"canonical\"";
return Results.Bytes("%PDF-1.4\n%%EOF\n"u8.ToArray(), "application/pdf");
});
$ curl -s -D - -o /dev/null http://localhost:20106/downloads/catalog.pdf | grep -iE "^HTTP|^link|^content-type"
HTTP/1.1 200 OK
Content-Type: application/pdf
Link: <https://bookstore.example/uploads/catalog-2026.pdf>; rel="canonical"
Google her sayfa için tek yöntem seçmeni öneriyor: aynı HTML sayfasında hem <link> hem Link header'ı varsa ve farklı URL'ler gösteriyorsa bu bir hatadır.
JavaScript ve Angular: Tek Canonical, İlk HTML'de
Google Aralık 2025'te JavaScript için canonical kuralını netleştirdi: canonical link ideal olarak HTML'de bulunmalı; JavaScript ile ayarlanıyorsa değeri orijinal HTML'deki canonical ile aynı olmalı, ya da orijinal HTML'de hiç canonical olmamalı. Birden fazla canonical tag'i üretmek veya mevcut olanı değiştirmek önerilmiyor. Pratikte iki tehlikeli senaryo var: index.html'e sabit yazılmış bir ana sayfa canonical'ı ile JavaScript'in sonradan değiştirdiği canonical, ve her navigasyonda yeni bir <link> ekleyip eskisini silmeyen kod.
Bookstore'un Angular SeoService'i canonical'ı DOCUMENT token'ı üzerinden yönetiyor; böylece aynı kod sunucuda da (SSR, Server-Side Rendering) tarayıcıda da çalışıyor ve hep tek bir element'i güncelliyor:
private setCanonical(url: string): void {
// tek bir canonical link: varsa güncelle, yoksa oluştur
let link = this.document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (!link) {
link = this.document.createElement('link');
link.setAttribute('rel', 'canonical');
this.document.head.appendChild(link);
}
link.setAttribute('href', url);
}
Service noindex verilen sayfalarda canonical'ı tamamen kaldırıyor; noindex ile canonical aynı sayfada çelişen iki sinyal olur. Sunucunun gönderdiği HTML'e bakınca:
$ curl -s http://localhost:20110/books/dune | grep -o '<link rel="canonical"[^>]*>'
<link rel="canonical" href="https://bookstore.example/books/dune">
$ curl -s http://localhost:20110/books/dune | grep -c 'rel="canonical"'
1
$ curl -s http://localhost:20110/client/books/dune | grep -c 'rel="canonical"'
0
Prerender edilen kitap sayfasında canonical ilk HTML'de ve tek. Aynı component'i RenderMode.Client ile servis eden route'ta ilk HTML'de canonical yok; JavaScript çalışınca ekleniyor. Bu, Google kuralının izin verdiği ikinci durum (orijinal HTML'de canonical yok), ama render'a bağımlı: Google sayfayı render etmezse canonical'ı hiç görmez. Index'lenmesi istenen sayfalarda ilk seçenek SSR veya prerender olmalı; bunu serinin sekizinci ve dokuzuncu yazılarında derinleştiriyoruz. DOCUMENT token'ının neden global document yerine kullanıldığı da orada.
Google'ın Seçtiği Canonical
Canonical'ı sen beyan edersin, Google seçer. Search Console'daki URL Inspection aracı, bir URL için Google'ın seçtiği canonical'ı gösteriyor; beyan ettiğinle farklıysa sebebini sinyallerde aramalısın. Google'ın canonicalization sorun giderme sayfası yaygın nedenleri sayıyor: hreflang olmadan farklı bölgelere neredeyse aynı içeriği sunmak, CMS (Content Management System) veya eklentilerin yanlış canonical ya da redirect üretmesi, bir domain'in içeriğini başka bir domain için servis eden yanlış yapılandırılmış sunucular, hack'lenmiş sitelere enjekte edilen cross-domain canonical'lar, içeriğin partner sitelerde yeniden yayımlanması ve izinsiz kopyalayan siteler.
Düzeltmeden sonra sabır gerekiyor: aynı doküman, Google'ın içerik sorunları düzeltildikten sonra bile sayfaları iki haftaya kadar aynı duplicate kümesinde tutabileceğini söylüyor; sayfalar arasındaki belirgin farklar kümelerin daha hızlı ayrılmasına yardım ediyor. Syndication (içeriğin partnerlerde yeniden yayımlanması) için öneri de dikkat çekici: partnerden canonical yerine index'lemeyi engellemesini istemek.
mertozen.com'da Canonical
Bu sitenin kaynak kodunda canonical, configuration'daki SiteUrl'den (https://www.mertozen.com) ve sayfanın kendi path'inden üretiliyor; Host header'ına bakılmıyor, yani yukarıdaki naif versiyonun sorunu yok. Blog listesinde canonical üretilirken parametreler sabit sırayla diziliyor, page=1 ve varsayılan sıralama atılıyor, site içi arama terimi (q) hiç girmiyor. Arama sonuç sayfası bu sayede kendi canonical'ını değil, terim atılmış listeyi gösteriyor.
Bir nokta tartışmaya açık: sıralama varsayılandan farklıysa (sort= parametresi) canonical'a giriyor. Aynı yazı listesinin farklı sıraları böylece birbirinden ayrı canonical URL'ler oluyor; Google'ın URL yapısı sayfası sıralama parametrelerini duplicate üreten parametreler arasında sayıyor. Yukarıdaki tag helper'ın yaptığı gibi sıralamayı canonical'dan atmak bu kümeyi tek URL'de toplar. Kategori, seri ve tag ise içeriği değiştirdiği için canonical'da kalmalı. Değişiklik kararı site sahibinin.
Türkçe ve İngilizce sayfalar ise doğru şekilde kendi canonical'larını taşıyor ve birbirine hreflang ile bağlanıyor; çok dilli kurulumun ayrıntısı serinin onuncu yazısında.
Üretimde Dikkat
Canonical'ı test et. Bir şablon hatası bütün sayfaların canonical'ını tek bir URL'ye çevirebilir ve bunu tarayıcıda görmezsin. Integration test'te birkaç temsilci URL için beklenen canonical'ı assert etmek ucuz bir sigorta.
Canonical URL'nin kendisi 200 dönmeli. Canonical'ı redirect eden, 404 dönen veya noindex'li bir URL'ye vermek, Google'a "doğru adres bu" deyip o adresi kapatmak demek. Site taşımalarından sonra en sık görülen hata budur: eski domain'i gösteren canonical'lar şablonda unutulur.
Sitemap, iç link'ler ve canonical aynı URL biçimini kullanmalı: aynı scheme, aynı host, aynı trailing slash kuralı. Serinin ilk yazısındaki normalizasyon middleware'i ile bu tag helper aynı kuralları uygulamalı; ikisini tek bir yardımcı class'tan beslemek kuralların ayrışmasını önler.
URL Inspection'daki "Google'ın seçtiği canonical" alanını önemli şablon tipleri için (kitap, kategori, yazar) arada bir kontrol et. Fark varsa önce sinyallerin tutarlılığına bak.
Sık Düşülen Tuzaklar
Bütün sayfaları ana sayfaya canonical'lamak. Sayfalar farklıysa Google sinyali yok sayar; saymazsa sayfalar index'ten düşer.
Canonical'ı noindex'li veya redirect eden bir URL'ye vermek. Sinyaller birbirini iptal eder.
Göreli canonical yazmak. href="/books/dune" çalışabilir ama Google mutlak URL öneriyor; göreli URL yanlış host veya protocol ile çözülebilir.
Canonical'ı istekten (Host, scheme, query) kopyalamak. Takip parametreleri, http ve yabancı host'lar canonical'a sızar.
JavaScript ile ikinci bir canonical eklemek ya da HTML'dekini değiştirmek. Google'ın Aralık 2025 kuralına göre JavaScript'teki değer HTML'dekiyle aynı olmalı veya HTML'de hiç olmamalı.
Sayfalamanın bütün sayfalarını birinci sayfaya canonical'lamak. İkinci ve sonraki sayfalardaki öğelerin keşfi zayıflar; her sayfa kendi canonical'ını taşımalı.
Küçük Bir Deneme
★☆☆ Kendi sitende beş URL'yi takip parametresi, büyük harf ve sondaki slash ekleyerek iste ve dönen canonical'ları karşılaştır. Hepsi aynı temiz URL'ye mi iniyor?
★★☆ CanonicalLinkTagHelper'a bir Href property'si ekle: sayfa isterse canonical'ı açıkça versin (ör. print sayfası /books/dune/print için /books/dune). Print sayfası için canonical'ı ve aynı sayfaya noindex verilip verilmemesi gerektiğini tartış.
★★★ Serinin ilk yazısındaki normalizasyon middleware'i ile bu tag helper'ı tek bir CanonicalUrlBuilder class'ına bağla: middleware gelen URL'yi bu class'la canonical'a çevirip farklıysa 301 versin, tag helper aynı class'ın sonucunu yazsın. Ardından takip parametrelerinin redirect edilmemesi (analitik verisi kaybolmasın) ama canonical'dan atılması gerektiği kuralını test ile göster.
Canonical, duplicate URL'ler arasında seçim yapar; ama bir URL'nin var olup olmadığını, taşınıp taşınmadığını ya da geçici olarak çalışmadığını status code söyler. Bir sonraki yazıda Google'ın 404, 410, 5xx ve 429'u nasıl yorumladığını, soft 404'ü ve SPA'larda (Single-Page Application) her şeye 200 dönme sorununu ASP.NET Core ve Angular SSR ile göreceğiz.