Site Taşıma ve Domain Değişikliği: URL Mapping, 301 Planı ve Change of Address
Site taşımada her eski URL tek hop'ta, 301 ile son hedefine gider; redirect'ler en az bir yıl kalır ve Change of Address yalnız domain değişiminde, www'lu ve www'suz her varyant için kullanılır. CSV mapping'den zincir kontrollü bir redirect tablosu ve her eski URL'yi 301, hedef 200 ve canonical açısından doğrulayan bir script yazıyor, mertozen.com'un /en/ geçişini bu listeyle okuyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10) ile derlenip denenmiştir; taşıma adımları Google'ın URL (Uniform Resource Locator) değişikliği içeren taşımalar rehberinin ("Site moves with URL changes") ve Change of Address aracı dokümanının Kasım 2026 hâli esas alınarak anlatılmıştır.
Önce Tahmin Et
Bookstore iki değişikliği aynı çeyreğe planlamış: İngilizce sayfalar ?lang=en parametresinden /en/ alt klasörüne taşınacak (serinin onuncu yazısındaki yapı) ve eski /catalog/ path'i /books/'a dönüşecek. Katalogdan çekilmiş birkaç kitabın eski URL'leri de var. Proje yöneticisinin listesinde üç madde: "Search Console'da Change of Address gönder", "redirect'leri üç ay tut, sonra kaldır", "hedefi olmayan eski URL'leri ana sayfaya yönlendir".
Üç maddenin hangisi doğru? Bu yazı serinin son yazısı ve önceki on bir yazının neredeyse hepsini (URL tasarımı, robots.txt, noindex, sitemap, canonical, status code, redirect, hreflang, log analizi) tek bir operasyonda birleştiriyor: site taşıma.
Taşıma Türleri
Google'ın rehberi URL değiştiren taşımaları ele alıyor ve hepsinin ortak noktası aynı: eski URL'ler yeni URL'lere kalıcı olarak yönlenecek. Fark, kapsamda ve Search Console'un rolünde:
| Tür | Örnek | Change of Address |
|---|---|---|
| Protocol | http:// → https:// | Gerekmez |
| Domain veya subdomain | bookstore-old.example → bookstore.example | Evet, eski domain'in her varyantı için |
| URL yapısı (aynı host) | ?lang=en → /en/, /catalog/ → /books/ | Kullanılamaz; araç host değişikliği içindir |
| Platform veya CMS (Content Management System) | Razor'dan Angular SSR'a (Server-Side Rendering), bir CMS'ten diğerine | Yalnız URL'ler veya host değişiyorsa ilgili satır |
Açılıştaki ilk maddenin cevabı tabloda: Change of Address aracı domain ve subdomain taşımaları için; aynı host içindeki bir path değişikliği için gönderilecek bir şey yok. Haziran 2026'da rehbere eklenen bir not da domain taşımalarıyla ilgili: aracı eski domain'in bütün subdomain varyantları için, www'lu ve www'suz sürümler dahil, ayrı ayrı kullanmak gerekiyor. Bunun için her varyantın Search Console'da doğrulanmış bir mülk olması lazım.
Hazırlık: URL Envanteri ve Mapping
Taşımanın en çok emek isteyen ve en çok atlanan kısmı, eski URL'lerin tam listesini çıkarmak. Google'ın rehberi kaynakları sayıyor: mevcut sitemap'ler, server log'ları, analytics verisi ve CMS. Sitemap yalnız bugün yayında olanları gösterir; yıllar önce kaldırılmış ama dış link'leri hâlâ duran URL'ler ancak log'larda çıkar. Serinin on birinci yazısındaki log aracı, son birkaç ayda istek almış bütün path'leri çıkarmak için de kullanılabilir.

Envanter çıktıktan sonra her eski URL'nin bir hedefi olmalı. İki tür kural var: pattern'le ifade edilebilenler (her ?lang=en URL'si aynı path'in /en/ sürümüne) ve tek tek eşleştirilmesi gerekenler (yeniden adlandırılan slug'lar, birleştirilen kategoriler). Pattern kuralları kodda veya rewrite kurallarında yaşar; tek tek eşleştirmeler ise bir tablo ister. Bookstore'un tablosu düz bir CSV (Comma-Separated Values):
old,new
/books/dune?lang=en,/en/books/dune
/books/dune?lang=tr,/books/dune
/books/foundation?lang=en,/en/books/foundation
/books/foundation?lang=tr,/books/foundation
/catalog/dune-1965,/books/dune
/catalog/neuromancer,/books/neuromancer
/catalog/hyperion,/books/hyperion
/gift-cards,/campaigns/gift-cards
Hedef seçerken Google'ın rehberindeki kural net: her eski URL doğrudan son hedefine gitmeli ve çok sayıda sayfayı alakasız tek bir hedefe, örneğin ana sayfaya, yönlendirmekten kaçınmalı. Taşınmayan içerik için doğru yanıt 404 veya 410. Açılıştaki üçüncü madde bu yüzden yanlış: hedefi olmayan URL'yi ana sayfaya yönlendirmek kullanıcıyı aradığı şeyden uzaklaştırır ve Google bu tür redirect'leri soft 404 olarak raporlayabilir.
CSV'den Redirect Tablosu
Tablo uygulama başlarken bir kez okunuyor. Okurken iki kontrol yapılıyor: aynı eski URL iki kez geçmesin ve bir hedef aynı zamanda başka bir satırın eski URL'si olmasın, yani tablo kendi içinde zincir kurmasın:
namespace Bookstore.Migration;
// eski URL (path + query) -> yeni path, açılışta mapping CSV'sinden bir kez yüklenir
public sealed class RedirectMap
{
private readonly Dictionary<string, string> _map = new(StringComparer.OrdinalIgnoreCase);
public static RedirectMap Load(string csvPath)
{
RedirectMap map = new();
foreach (string line in File.ReadLines(csvPath).Skip(1))
{
string[] cols = line.Split(',');
if (cols.Length != 2) continue;
if (!map._map.TryAdd(cols[0].Trim(), cols[1].Trim()))
throw new InvalidOperationException($"duplicate old URL in mapping: {cols[0]}");
}
// hedefler eski URL olmamalı: bu bir zincir olurdu
foreach ((string oldUrl, string target) in map._map)
if (map._map.ContainsKey(target))
throw new InvalidOperationException($"chain: {oldUrl} -> {target} -> {map._map[target]}");
return map;
}
public bool TryGet(string pathAndQuery, out string target) => _map.TryGetValue(pathAndQuery, out target!);
public IEnumerable<KeyValuePair<string, string>> All => _map;
}
Tabloya /dune-classic,/catalog/dune-1965 satırını ekleyince uygulama açılmadan duruyor:
Unhandled exception. System.InvalidOperationException: chain: /dune-classic -> /catalog/dune-1965 -> /books/dune
Hatayı production'da bir redirect zinciri olarak değil, deploy'dan önce bir exception olarak görmek istiyorsun. Middleware, routing'den önce tabloya bakıp eşleşme varsa 301 dönüyor:
// 1) mapping tablosundaki tek tek taşımalar, routing'den önce kontrol edilir
app.Use(async (context, next) =>
{
string pathAndQuery = context.Request.Path + context.Request.QueryString;
if (map.TryGet(pathAndQuery, out string target))
{
context.Response.StatusCode = StatusCodes.Status301MovedPermanently;
context.Response.Headers.Location = target;
return;
}
await next();
});
$ curl -sI "http://localhost:20114/books/dune?lang=en" | grep -iE "^HTTP|^location"
HTTP/1.1 301 Moved Permanently
Location: /en/books/dune
Tablo büyükse (on binlerce satır) bellekteki bir Dictionary hâlâ en hızlı çözüm; satır sayısı yüz binlere çıkıyorsa veya tablo deploy'dan bağımsız güncellenecekse veritabanına taşı ve serinin yedinci yazısındaki slug geçmişi tablosu gibi sorgula. Query string'li eşleşmede parametre sırası önemli: ?lang=en&page=2 ile ?page=2&lang=en aynı URL değil. Parametreli eski URL'ler çoksa tablo yerine pattern kuralı (dil parametresini at, gerisini taşı) daha sağlam.
Doğrulama Script'i: Her Eski URL Tek Hop'ta Doğru Yere
Redirect'lerin varlığı yetmez; doğru yere gittiklerini ve gittikleri yerin sağlıklı olduğunu kanıtlamak gerekir. Bookstore'un doğrulayıcısı tablodaki her eski URL için üç şeyi kontrol ediyor: ilk yanıt 301 mi, Location beklenen hedef mi, hedef 200 dönüyor ve canonical'ı kendisi mi?
// taşımadan sonra: her eski URL doğrudan hedefine 301 vermeli, hedef de 200 dönen bir canonical sayfa olmalı
public static partial class Verifier
{
public static async Task<int> RunAsync(RedirectMap map, Uri site)
{
using HttpClient http = new(new HttpClientHandler { AllowAutoRedirect = false }) { BaseAddress = site };
int failures = 0;
foreach ((string oldUrl, string expected) in map.All)
{
using HttpResponseMessage first = await http.GetAsync(oldUrl);
string location = first.Headers.Location?.OriginalString ?? "-";
string problem = "";
if (first.StatusCode != HttpStatusCode.MovedPermanently)
problem = $"expected 301, got {(int)first.StatusCode}";
else if (location != expected)
problem = $"Location {location}, expected {expected}";
else
{
using HttpResponseMessage target = await http.GetAsync(expected);
string html = await target.Content.ReadAsStringAsync();
string? canonical = CanonicalRegex().Match(html) is { Success: true } m ? m.Groups[1].Value : null;
if (target.StatusCode != HttpStatusCode.OK)
problem = $"target returns {(int)target.StatusCode}";
else if (canonical != new Uri(new Uri("https://bookstore.example"), expected).ToString())
problem = $"target canonical is {canonical ?? "missing"}";
}
failures += problem.Length > 0 ? 1 : 0;
Console.WriteLine($"{(problem.Length > 0 ? "FAIL" : "ok "),-5} {oldUrl,-28} {(int)first.StatusCode} -> {location,-24} {problem}");
}
Console.WriteLine($"\n{map.All.Count()} URLs checked, {failures} failed");
return failures == 0 ? 0 : 1;
}
[GeneratedRegex("<link rel=\"canonical\" href=\"([^\"]+)\"")]
private static partial Regex CanonicalRegex();
}
AllowAutoRedirect = false kritik: HttpClient varsayılan olarak redirect'leri izliyor ve zinciri gizliyor. Script'i taşınmış uygulamaya karşı çalıştırdım:
$ dotnet run -- verify http://localhost:20114
ok /books/dune?lang=en 301 -> /en/books/dune
ok /books/dune?lang=tr 301 -> /books/dune
ok /books/foundation?lang=en 301 -> /en/books/foundation
ok /books/foundation?lang=tr 301 -> /books/foundation
ok /catalog/dune-1965 301 -> /books/dune
ok /catalog/neuromancer 301 -> /books/neuromancer
FAIL /catalog/hyperion 301 -> /books/hyperion target returns 404
FAIL /gift-cards 301 -> /campaigns/gift-cards target canonical is https://bookstore.example/campaigns
8 URLs checked, 2 failed
İki hata, taşımalarda en sık görülen iki hatanın aynısı. Hyperion katalogdan çekilmiş; tabloya yine de bir hedef yazılmış ve hedef 404 dönüyor. Doğrusu bu satırı tablodan çıkarıp eski URL'ye doğrudan 410 dönmek; 301 ardından 404, hem bir hop israfı hem de kullanıcı için kırık bir yolculuk. Hediye kartı sayfası ise yeni adreste çalışıyor ama canonical'ı başka bir sayfayı gösteriyor; şablondaki eski bir değer. Google redirect'i izler, sonra canonical'ı görür ve iki sinyal çelişir. Script sıfırdan farklı exit code döndüğü için CI'da (Continuous Integration) ve deploy pipeline'ında bir kapı olarak kullanılabiliyor.
Geçiş Günü ve Change of Address
Hazırlık bittiyse geçiş günü kısa bir kontrol listesi. Redirect'leri aç; mümkünse 301 veya 308, sunucu tarafında. Doğrulama script'ini bu kez production'a karşı çalıştır; staging'de geçen bir test, production'daki reverse proxy, CDN (Content Delivery Network) veya IIS (Internet Information Services) kuralları yüzünden farklı sonuç verebilir. Yeni sitenin robots.txt'sinin staging'den kalan bir Disallow: / taşımadığını ve sayfalarda staging'den kalma bir noindex olmadığını kontrol et; taşımaların en pahalı hatası budur. Yeni URL'leri içeren sitemap'i Search Console'a gönder. Google'ın rehberi hem eski hem yeni URL'ler için sitemap hazırlamayı öneriyor; eski URL'leri bir süre ayrı bir sitemap'te tutmak, Google'ın redirect'leri daha çabuk keşfetmesine yardım edebilir.
Domain değiştiyse Search Console'daki Change of Address aracını eski mülkte kullan. Araç Google'a taşımayı doğrudan bildiriyor ve redirect'lerin yanında ek bir sinyal; ama redirect'lerin yerini tutmuyor. Haziran 2026 notunu hatırla: www.bookstore-old.example ve bookstore-old.example iki ayrı mülk olarak doğrulanıp ikisi için de ayrı ayrı gönderilmeli.
İzleme: Ne Beklemeli?
Google'ın rehberi beklentiyi dürüstçe söylüyor: taşıma sırasında sıralamada geçici dalgalanma beklenir; küçük ve orta ölçekli sitelerde sayfaların çoğunun taşınması birkaç hafta sürebilir, büyük sitelerde daha uzun. Kalıcı redirect'ler PageRank kaybettirmiyor. Bir süre sonuçlarda hâlâ eski URL'nin görünmesi de normal; Google eski URL'yi canonical'ın alternatif adı olarak bir süre gösterebiliyor ve bu kendiliğinden sönüyor.
İzlenecekler: Search Console'un "Sayfalar" raporu (eski URL'ler "Page with redirect" satırına geçmeli), Sitemap raporu (yeni sitemap'in URL'leri index'e girmeli), Crawl Stats (taşıma sonrası crawl isteklerinde geçici bir artış normal) ve server log'ları. Log'da eski URL'lere gelen isteklerin oranı haftalar içinde düşmeli; düşmüyorsa bir yerde, bir şablonda, bir sitemap'te veya bir dış sistemde eski URL'ler hâlâ yayılıyor demektir. Açılıştaki ikinci madde de burada düşüyor: Google redirect'leri en az bir yıl tutmayı öneriyor. Dış link'ler, yer imleri ve e-postalardaki link'ler yıllarca yaşadığı için redirect'leri kaldırmak için iyi bir sebep yoksa süresiz tutmak daha güvenli.
mertozen.com: ?lang='den /en/'e Geçişin Taşıma Planı
Serinin onuncu yazısında anlattığım /en/ geçişi tam bir URL yapısı taşıması: aynı host, bütün İngilizce URL'ler değişti. Sitenin kaynak kodundaki v1.12.0–v1.12.3 sürümleri bu yazının kontrol listesine göre okunduğunda:
- Mapping pattern'le ifade edilmiş:
?lang=en→ aynı path'in/en/sürümü,?lang=tr→ parametresiz path, diğer query parametreleri korunuyor. Tablo gerekmemiş. - Redirect'ler 301 ve tek hop;
/en→/en/de 301. - Canonical, hreflang, sitemap ve iç link'ler yeni yapıya göre güncellenmiş; CMS içeriğindeki link'ler İngilizce sayfada render sırasında
/enönekine çevriliyor. - Change of Address gerekmiyor; host değişmedi.
- Taşıma sonrası bir yan etki v1.12.3'te düzeltilmiş:
~/ile verilen CSS (Cascading Style Sheets), JavaScript ve font yolları İngilizce sayfalarda/en/css/site.cssgibi üretiliyordu; dosyalar çalışıyordu ama tarayıcı aynı dosyayı Türkçe ve İngilizce için ayrı ayrı indirip cache'liyordu. Yollar köke çevrildi.
Kaynak koddan göremediğim, dolayısıyla açık kalan adımlar: yukarıdaki doğrulama script'inin production'a karşı çalıştırılması (eski ?lang= URL'lerinin listesi önceki sitemap'lerden ve log'lardan çıkarılabilir), eski URL'ler için geçici bir sitemap, Search Console'da "Page with redirect" ve "Sayfalar" raporlarının birkaç hafta izlenmesi ve log'da lang= içeren isteklerin seyrinin takibi. Bu yazı için canlı siteye tarama yapmadım; yukarıdaki bilgiler sitenin kaynak kodundan ve commit geçmişinden. Ne zaman ne yapılacağı site sahibinin kararı.
Üretimde Dikkat
Taşıma ile içerik değişikliğini aynı güne koyma. URL'ler, şablonlar ve içerik aynı anda değişirse trafikteki bir değişikliğin nedenini ayıramazsın. Taşımayı tek başına, ölçülebilir bir değişiklik olarak yap.
Taşımayı trafiğin düşük olduğu bir döneme planla ve geri dönüş planın olsun. Redirect'leri bir configuration bayrağıyla açıp kapatabilmek, beklenmeyen bir hata durumunda dakikalar içinde eski hâle dönmeni sağlar; ama bir kez açıldıktan sonra geri almak Google için ikinci bir taşıma demek, eşiği yüksek tut.
Eski domain'i bırakma. Domain taşımasında eski domain'in süresinin dolmasına izin vermek, redirect'lerin de bitmesi demek; üstelik domain başkasının eline geçerse eski link'ler onun sitesine gider. Rehber satın alınan kullanılmış domain'ler için de bir uyarı veriyor: önceki sahibinden kalan manual action'ları ve URL kaldırma isteklerini kontrol et.
Taşınmayan içeriği bilinçli olarak 404 veya 410'a bırak ve bunu mapping tablosunda açıkça işaretle (ör. boş hedef). "Hedefi unutulmuş URL" ile "kasıtlı olarak kaldırılmış URL" ayrımı, doğrulama script'inin hataları anlamlı raporlamasını sağlar.
Sık Düşülen Tuzaklar
Redirect'leri birkaç ay sonra kaldırmak. Google en az bir yıl öneriyor; dış link'ler daha uzun yaşar.
Hedefi olmayan her şeyi ana sayfaya yönlendirmek. Soft 404 olarak raporlanabilir; 404 veya 410 dön.
Eski → ara → yeni zincirleri. Önceki taşımaların redirect'leri yeni taşımayla zincirlenir; her eski URL'yi doğrudan son hedefe bağla.
Staging'den kalan noindex veya Disallow: /. Taşımanın en pahalı ve en sık hatası.
Aynı host içindeki path değişikliği için Change of Address aramak ya da domain taşımasında www'lu ve www'suz varyantlardan birini unutmak.
Doğrulamayı yalnız staging'de yapmak. Production'daki proxy, CDN ve sunucu kuralları sonucu değiştirebilir.
Küçük Bir Deneme
★☆☆ Kendi sitende son bir aylık log'dan istek almış bütün path'leri çıkar ve bugünkü sitemap'le karşılaştır. Sitemap'te olmayan ama istek alan path'ler, bir taşımada unutulacak URL adaylarıdır.
★★☆ RedirectMap'e boş hedef desteği ekle: hedefi boş satırlar 410 dönsün. Doğrulama script'ini bu satırlar için 410 bekleyecek şekilde güncelle ve Hyperion satırını buna çevir.
★★★ Doğrulama script'ini bir crawler'a dönüştür: yeni sitenin sitemap'ini okuyup her URL'yi istesin, sayfadaki bütün iç link'leri toplasın ve eski URL pattern'ine (?lang=, /catalog/) uyan bir link bulursa, hangi sayfada olduğunu raporlasın. Böylece redirect'lerin yanında, sitenin kendisinin hâlâ eski URL'lere link verip vermediğini de görürsün.
Seri burada bitiyor. URL'den başlayıp robots.txt, noindex, sitemap, canonical, status code, redirect, JavaScript rendering, Angular SSR, hreflang ve log analizi üzerinden site taşımaya geldik; her adımda Google'ın kendi dokümanını ve kendi uygulamamızın gerçek yanıtlarını esas aldık. Buradan sonraki doğal duraklar, structured data ile sonuç sayfasındaki görünümü zenginleştirmek ilerideki Structured Data ve Schema.org serisinde, sayfa hızı ve Core Web Vitals ilerideki Web Performansı serisinde; AI (Artificial Intelligence) yanıt motorlarının siteyi nasıl okuduğu ise GEO (Generative Engine Optimization) serisinde.