Mert Özen Her Satırda Daha İleri
SEO ve GEO Teknik SEO — #04

XML Sitemap ve IndexNow: ASP.NET Core'da Dinamik Sitemap Üretmek ve Değişikliği Bildirmek

Mert Özen 8 Kas 2026 13 dk 2 görüntülenme
XML Sitemap ve IndexNow: ASP.NET Core'da Dinamik Sitemap Üretmek ve Değişikliği Bildirmek

Sitemap Google için bir ipucudur: changefreq ve priority yok sayılır, lastmod yalnız doğrulanabilir şekilde doğruysa kullanılır; bir dosya en fazla 50.000 URL ve 50 MB. EF Core'dan streaming sitemap üretip 108.000 URL ile ölçüyor, output cache'i yayında temizliyor, Bing ve Yandex için IndexNow'u kuruyor, Angular'da build-time sitemap ve mertozen.com'un sitemap'ini inceliyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (ASP.NET Core 10), EF (Entity Framework) Core 10.0.14 (Microsoft.EntityFrameworkCore.Sqlite) ve Angular 22.2 ile derlenip denenmiştir; protokol bilgileri sitemaps.org, Google Search Central ve indexnow.org'un Kasım 2026 hâlinden alınmıştır.

Önce Tahmin Et

Bookstore'un kataloğunda 120.000 kitap var; bunların 108.000'i yayında, gerisi taslak. İlk sitemap'i bir stajyer yazmış: her URL'de (Uniform Resource Locator) <changefreq>daily</changefreq>, <priority>1.0</priority> ve <lastmod> olarak sitemap'in üretildiği an. Her yayından sonra da Google'ın sitemap ping adresine bir istek atıyor.

Üç soru. Google bu sitemap yüzünden kitap sayfalarını her gün crawl eder mi? 108.000 URL tek bir dosyaya sığar mı? Ve ping isteği bugün ne işe yarıyor? Cevaplar protokolün kendisinde ve Google'ın sitemap dokümanında.

Serinin önceki iki yazısında crawler'ı durduran ve index'ten çıkaran araçlara baktık. Sitemap ters yönde çalışır: "şu URL'ler var ve şu tarihte değişti" bilgisini verir. Bu yazıda XML (Extensible Markup Language) sitemap'i ASP.NET Core'da EF Core'dan streaming olarak üretip ölçeceğiz, değişikliği anında bildirmek için IndexNow'u kuracağız.

Protokol: Sınırlar ve Google'ın Okuduğu Alanlar

Sitemap protokolü sitemaps.org'da tanımlı. Temel kurallar:

  • Bir sitemap dosyası en fazla 50.000 URL ve sıkıştırılmamış hâliyle en fazla 50 MB olabilir. Daha büyük siteler dosyaları böler ve bir sitemap index dosyasıyla listeler.
  • Dosya UTF-8 (Unicode Transformation Format, 8-bit) kodlanır; URL'ler protocol ve host dahil mutlak yazılır. Google URL'leri yazıldıkları gibi crawl ediyor.
  • Sitemap'in kapsamı, Search Console'dan gönderilmediyse, bulunduğu klasör ve altıdır. Google bu yüzden dosyayı kökte tutmayı öneriyor.
  • Dosya gzip ile sıkıştırılıp .xml.gz olarak da sunulabilir; 50 MB sınırı sıkıştırılmamış boyut için geçerli.

Asıl ilginç kısım, her alanın Google için ne anlama geldiği:

AlanProtokoldeki anlamıGoogle'ın kullanımı
locSayfanın URL'siCrawl edilecek URL; canonical olmalı
lastmodSon değişiklik tarihiYalnız tutarlı ve doğrulanabilir şekilde doğruysa kullanılır
changefreqTahmini değişim sıklığıYok sayılır
prioritySite içi göreli önemYok sayılır

Açılıştaki ilk sorunun cevabı burada: changefreq ve priority Google için hiçbir şey ifade etmiyor; yazmak zararsız ama boşuna byte. Google sitemap göndermenin de yalnızca bir ipucu olduğunu, dosyanın indirileceğinin veya her URL'nin crawl edileceğinin garantisi olmadığını açıkça söylüyor. Bing'in ve diğer motorların bu alanlarla ne yaptığı kendi dokümanlarının konusu; tek bir sitemap ile hepsine hizmet ettiğin için protokolde geçerli olan alanları yazmak yasak değil, yalnız Google'dan etki bekleme.

lastmod'u Dürüst Kullanmak

Google'ın lastmod kuralı şu: değer "tutarlı ve doğrulanabilir şekilde" doğruysa kullanılır; doğrulama, sayfanın gerçek son değişikliğiyle karşılaştırma demek. Ana içerik, structured data veya link'lerdeki önemli bir değişiklik sayılır; footer'daki telif yılının güncellenmesi sayılmaz. Stajyerin sitemap'i her URL'ye üretim anını yazıyor. Google birkaç crawl'da bu tarihlerin sayfalardaki gerçek değişikliklerle örtüşmediğini görünce sitenin lastmod değerlerine güvenmeyi bırakır; asıl kayıp, gerçekten değişen sayfanın sinyali de gürültüye karışır.

Bunun veri modelinde bir karşılığı var. Çoğu tabloda bir UpdatedAt kolonu bulunur ve stok sayısı, görüntülenme sayacı, iç bir flag değiştiğinde de güncellenir. Bunlar sayfanın içeriğini değiştirmez. Bookstore'da sitemap için ayrı bir alan tutuyoruz:

public sealed class Book
{
    public int Id { get; set; }
    public required string Slug { get; set; }
    public required string Title { get; set; }
    public bool IsPublished { get; set; }
    public DateTime PublishedAt { get; set; }
    public DateTime? ContentUpdatedAt { get; set; } // yalnız gerçek içerik değişiklikleri
}

lastmod = ContentUpdatedAt ?? PublishedAt. Kitabın açıklaması, başlığı veya yazar bilgisi değiştiğinde ContentUpdatedAt güncellenir; stok değiştiğinde güncellenmez. Tarih biçimi W3C (World Wide Web Consortium) Datetime; gün düzeyi (2026-11-08) yeterli.

ASP.NET Core'da Streaming Sitemap

108.000 URL için sitemap'i bir XDocument olarak bellekte kurup string'e çevirmek çalışır, ama her istek onlarca MB geçici bellek ve büyük object heap baskısı demek. Doğrudan response stream'ine yazan bir XmlWriter ve EF Core'dan satır satır okuyan bir IAsyncEnumerable bu maliyeti ortadan kaldırıyor:

public static class SitemapWriter
{
    public const int UrlsPerFile = 50_000;
    private const string Ns = "http://www.sitemaps.org/schemas/sitemap/0.9";
    private const string Xhtml = "http://www.w3.org/1999/xhtml";
    private const string Site = "https://bookstore.example";

    // async yazma, dosya başında BOM yok
    private static readonly XmlWriterSettings Settings = new() { Async = true, Encoding = new UTF8Encoding(false) };

    public static async Task WriteBooksAsync(Stream output, BookstoreDb db, int part, CancellationToken ct)
    {
        IAsyncEnumerable<(string Slug, DateTime LastMod)> rows = db.Books
            .AsNoTracking()
            .Where(b => b.IsPublished)
            .OrderBy(b => b.Id)
            .Skip((part - 1) * UrlsPerFile)
            .Take(UrlsPerFile)
            .Select(b => ValueTuple.Create(b.Slug, b.ContentUpdatedAt ?? b.PublishedAt))
            .AsAsyncEnumerable();

        await using XmlWriter xml = XmlWriter.Create(output, Settings);
        await xml.WriteStartDocumentAsync();
        await xml.WriteStartElementAsync(null, "urlset", Ns);
        await xml.WriteAttributeStringAsync("xmlns", "xhtml", null, Xhtml);

        int written = 0;
        await foreach ((string slug, DateTime lastMod) in rows.WithCancellation(ct))
        {
            string tr = $"{Site}/books/{slug}";
            string en = $"{Site}/en/books/{slug}";
            await xml.WriteStartElementAsync(null, "url", Ns);
            await xml.WriteElementStringAsync(null, "loc", Ns, tr);
            await xml.WriteElementStringAsync(null, "lastmod", Ns, lastMod.ToString("yyyy-MM-dd"));
            await WriteAlternateAsync(xml, "tr", tr);
            await WriteAlternateAsync(xml, "en", en);
            await WriteAlternateAsync(xml, "x-default", tr);
            await xml.WriteEndElementAsync();

            if (++written % 1000 == 0)
                await xml.FlushAsync(); // client'a parça parça gönder, 10 MB'ı biriktirme
        }
        await xml.WriteEndElementAsync();
    }
}

Üç ayrıntı. Birincisi, ASP.NET Core response body'ye senkron yazmayı varsayılan olarak yasaklıyor (AllowSynchronousIO kapalı); bu yüzden XmlWriter'ın bütün yazma çağrıları async. İkincisi, Encoding ayarlanmazsa XmlWriter dosyanın başına bir BOM (Byte Order Mark) koyuyor; ilk denememde index dosyası görünmez bir U+FEFF karakteriyle başlıyordu. Çoğu parser bunu tolere eder, ama gerek yok. Üçüncüsü, her URL'deki xhtml:link satırları hreflang bilgisini sitemap'te veriyor; Bookstore'un Türkçe ve İngilizce sürümleri olduğu için. hreflang'ın kuralları serinin onuncu yazısında; burada yalnız dosya boyutuna etkisine bakacağız.

Index dosyası, yayındaki kitap sayısından parça sayısını hesaplıyor; endpoint'ler de output cache policy'siyle bağlanıyor:

builder.Services.AddOutputCache(o =>
    o.AddPolicy("sitemap", p => p.Expire(TimeSpan.FromHours(1)).Tag("sitemap")));

app.MapGet("/sitemap.xml", async (HttpContext ctx, BookstoreDb db) =>
{
    ctx.Response.ContentType = "application/xml; charset=utf-8";
    await SitemapWriter.WriteIndexAsync(ctx.Response.Body, db, ctx.RequestAborted);
}).CacheOutput("sitemap");

app.MapGet("/sitemaps/books-{part:int:min(1)}.xml", async (int part, HttpContext ctx, BookstoreDb db) =>
{
    ctx.Response.ContentType = "application/xml; charset=utf-8";
    await SitemapWriter.WriteBooksAsync(ctx.Response.Body, db, part, ctx.RequestAborted);
}).CacheOutput("sitemap");

SQLite veritabanına 120.000 kitap (yüzde onu taslak) seed edip istekleri attım:

$ curl -s http://localhost:20105/sitemap.xml
<?xml version="1.0" encoding="utf-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>https://bookstore.example/sitemaps/books-1.xml</loc></sitemap><sitemap><loc>https://bookstore.example/sitemaps/books-2.xml</loc></sitemap><sitemap><loc>https://bookstore.example/sitemaps/books-3.xml</loc></sitemap></sitemapindex>

books-1: 200 19210154 bytes 0.369588s
books-2: 200 19290150 bytes 0.284150s
books-3: 200 3112150 bytes 0.047874s
books-1 again: 19210154 bytes 0.011258s

books-1 urls:    50000
books-2 urls:    50000
books-3 urls:     8000

$ gzip -c books-1.xml | wc -c
  764545

Açılıştaki ikinci sorunun cevabı: 108.000 URL tek dosyaya sığmaz, üç dosyaya bölünüyor. Boyut tarafı da öğretici: hreflang'lı 50.000 URL 19,2 MB tutuyor. URL başına yaklaşık 384 byte; bunun büyük kısmı üç xhtml:link satırı. 50 MB sınırına uzak, ama URL'ler uzunsa ve dil sayısı artarsa 50.000 URL'den önce boyut sınırına çarpabilirsin; o durumda dosya başına URL sayısını düşür. gzip aynı dosyayı 765 KB'a indiriyor; büyük sitemap'lerde response compression açmak ya da .xml.gz sunmak bant genişliğini ciddi azaltır. Süreler bu makinedeki (Apple'ın M5 (Apple Silicon) işlemcili bir Mac, yerel SQLite dosyası) tek ölçümler; ilk istek 0,37 saniye, output cache'ten dönen ikinci istek 0,011 saniye.

Output Cache ve Yayın Anında Invalidation

Sitemap'i her istekte veritabanından üretmek gerekmiyor; ama bir saatlik cache, yeni yayımlanan kitabın sitemap'e bir saat geç girmesi demek. Tag'li output cache ile ikisini birden alıyoruz: yayın endpoint'i sitemap tag'ini temizliyor.

app.MapPost("/admin/books/{id:int}/publish", async (int id, BookstoreDb db, IOutputCacheStore cache,
    IndexNowClient client, CancellationToken ct) =>
{
    Book? book = await db.Books.FindAsync([id], ct);
    if (book is null) return Results.NotFound();

    book.IsPublished = true;
    book.PublishedAt = DateTime.UtcNow;
    await db.SaveChangesAsync(ct);

    await cache.EvictByTagAsync("sitemap", ct); // bir sonraki sitemap isteği taze
    await client.SubmitAsync([$"https://bookstore.example/books/{book.Slug}"], ct);
    return Results.NoContent();
});
$ curl -s -X POST -o /dev/null -w "publish: %{http_code}\n" http://localhost:20105/admin/books/10/publish
publish: 204
books-1 after publish: 19210142 bytes 0.348453s
<url><loc>https://bookstore.example/books/book-10</loc><lastmod>2026-11-08</lastmod>

Yayından sonraki istek cache'e takılmadan 0,35 saniyede yeniden üretildi ve book-10 bugünün tarihiyle dosyada. Dosya boyutunun küçülmesi dikkatini çekmeli: yeni bir URL eklendi ama dosya 12 byte küçüldü. Çünkü parçalar Skip/Take ile bölünüyor; araya giren her kitap sınırdaki bir kitabı bir sonraki dosyaya itiyor. Yani her yayın üç dosyanın içeriğini de değiştiriyor. Bunun daha stabil alternatifi, parçaları Id aralığıyla bölmek (Id 1 ile 50.000 arası birinci dosya gibi): dosyalar 50.000'den az URL içerebilir ama yeni bir kitap yalnız son dosyayı değiştirir.

Output cache'in iki varsayılan sınırını bilmek de gerekiyor. OutputCacheOptions.MaximumBodySize varsayılanı 64 MB; bundan büyük yanıtlar hiç cache'lenmez. SizeLimit varsayılanı 100 MB; üç sitemap parçası tek başına 41,6 MB tutuyor ve aynı cache'i paylaşan diğer endpoint'lerle birlikte bu sınıra yaklaşmak kolay. Output caching'in mekaniği ASP.NET Core Web API (Application Programming Interface) serisinin ".NET 7 ile ASP.NET Core Yenilikleri: MapGroup, TypedResults, Rate Limiting ve Output Caching" yazısında.

IndexNow: Değişikliği Anında Bildirmek

Açılıştaki üçüncü sorunun cevabı: Google'ın sitemap ping endpoint'i 2023'te kullanımdan kaldırıldı; o adrese istek atmak bugün bir şey ifade etmiyor. Google sitemap'i Search Console'dan göndermeyi, robots.txt'deki Sitemap satırını ve Search Console API'sini öneriyor; değişiklikten sonra Google'ın sitemap'i tekrar ne zaman okuyacağına kendisi karar veriyor.

IndexNow farklı bir yaklaşım: site, değişen URL'leri katılımcı arama motorlarından birine doğrudan POST eder. Protokolün sitesinde katılımcı olarak Microsoft Bing, Naver, Seznam.cz, Yandex ve Yep listeleniyor; birine gönderilen URL'ler diğer katılımcılarla paylaşılıyor. Google bu listede yok. Yani IndexNow, Bing ve Yandex trafiği için faydalı, Google için sitemap'in yerini tutmuyor.

Protokolün iki parçası var. Sahiplik doğrulaması için bir key: 8 ile 128 karakter arası, harf, rakam ve tire içeren bir değer; sitenin kökünde {key}.txt adlı ve içinde yalnız key olan bir dosyada yayımlanıyor. Bildirim ise JSON (JavaScript Object Notation) body'li bir POST; tek istekte en fazla 10.000 URL:

public sealed record IndexNowOptions(string Endpoint, string Host, string Key);

public sealed class IndexNowClient(HttpClient http, IndexNowOptions options, ILogger<IndexNowClient> logger)
{
    public async Task SubmitAsync(IReadOnlyList<string> urls, CancellationToken ct)
    {
        // protokol sınırı: POST başına en fazla 10.000 URL
        foreach (string[] batch in urls.Chunk(10_000))
        {
            var payload = new
            {
                host = options.Host,
                key = options.Key,
                keyLocation = $"https://{options.Host}/{options.Key}.txt",
                urlList = batch,
            };
            using HttpResponseMessage response = await http.PostAsJsonAsync(options.Endpoint, payload, ct);
            logger.LogInformation("IndexNow {Count} URLs -> {Status}", batch.Length, (int)response.StatusCode);
        }
    }
}
// IndexNow key dosyası: https://bookstore.example/{key}.txt
app.MapGet($"/{indexNow.Key}.txt", () => Results.Text(indexNow.Key, "text/plain; charset=utf-8"));

Bu yazıdaki örnek bir arama motoruna gerçek istek göndermiyor; bookstore.example bizim domain'imiz değil ve gerçek motor key dosyasını o adreste bulamayacağı için isteği reddederdi. Endpoint'i aynı uygulamadaki, gelen body'yi loglayan bir taklide yönlendirdim. Yukarıdaki yayın isteğinin gönderdiği gerçek payload:

mock IndexNow received: {"host":"bookstore.example","key":"5f3c9a1e7b2d4c6f8a0e1b3d5c7f9a2e","keyLocation":"https://bookstore.example/5f3c9a1e7b2d4c6f8a0e1b3d5c7f9a2e.txt","urlList":["https://bookstore.example/books/book-10"]}
info: Bookstore.Sitemap.IndexNowClient[0]
      IndexNow 1 URLs -> 202

Gerçek bir motorun döndürebileceği kodlar protokolde tanımlı: 200 alındı, 202 alındı ama key doğrulaması bekliyor, 400 geçersiz biçim, 403 key geçersiz (dosya yok veya içerik tutmuyor), 422 URL'ler host'a ait değil, 429 çok fazla istek. 200 yalnızca isteğin alındığını söyler; URL'nin crawl edileceğini veya index'leneceğini değil. Production'da bu çağrıyı yayın isteğinin içinde senkron yapmak yerine bir background job'a ya da outbox'a bırak; motorun yavaş yanıtı editörün yayın butonunu bekletmemeli.

Angular SSR Projesinde Build-Time Sitemap

SSR (Server-Side Rendering) kullanan bir Angular uygulamasında içerik build anında belliyse (prerender edilen route'lar), sitemap da build'in bir adımı olabilir. Angular CLI (Command Line Interface) prerender ettiği route'ları dist/bookstore-ng/prerendered-routes.json dosyasına yazıyor; küçük bir Node.js script'i bunu sitemap'e çeviriyor:

// `ng build` sonrası: prerender edilen route'ları dist/.../browser/sitemap.xml'e çevir
import { readFileSync, statSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

const SITE = 'https://bookstore.example';
const dist = 'dist/bookstore-ng';
const { routes } = JSON.parse(readFileSync(join(dist, 'prerendered-routes.json'), 'utf8'));

const urls = Object.keys(routes).map(route => {
  // lastmod: üretilen HTML'in build zamanı bir içerik değişikliği değil, bu yüzden yazmıyoruz
  return `  <url><loc>${SITE}${route === '/' ? '/' : route}</loc></url>`;
});

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.join('\n')}
</urlset>
`;
writeFileSync(join(dist, 'browser', 'sitemap.xml'), xml);
console.log(`sitemap.xml: ${urls.length} URLs, ${statSync(join(dist, 'browser', 'sitemap.xml')).size} bytes`);
$ npm run build
> ng build && node scripts/sitemap.mjs
Prerendered 4 static routes.
sitemap.xml: 4 URLs, 357 bytes

$ curl -sI http://localhost:20110/sitemap.xml | grep -iE "^HTTP|content-type|cache-control"
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Content-Type: application/xml

Script lastmod yazmıyor, çünkü elindeki tek tarih build zamanı; her deploy'da bütün URL'lerin "değişti" demesi, yukarıda anlattığım güvensiz lastmod'un ta kendisi. Gerçek tarihleri veri kaynağından (CMS veya API) alabiliyorsan ekle. Cache header'ı ise önceki yazıda server.ts'e eklediğimiz setHeaders kuralından geliyor; o kural olmasa sitemap de bir yıllık cache'le servis edilirdi. Sunucuda render edilen ya da sık değişen içerikte build-time sitemap yetmez; Express tarafında bir route ile veya arkadaki ASP.NET Core API'sinden üretmek gerekir.

mertozen.com'un sitemap.xml'i

Bu sitenin sitemap'i de bir controller action'ından, XDocument ile üretilip bir saat IMemoryCache'te tutuluyor. Kaynak koddaki hâline göre: her sayfa için Türkçe ve İngilizce iki <url> kaydı, her birinde tr, en ve x-default alternate'leri; yazılar için lastmod = yayın tarihi ile CMS'teki (Content Management System) güncelleme tarihinin yenisi. Bu, yukarıda anlattığım doğru yaklaşım. Üç gözlem:

Birincisi, her kayıtta changefreq ve priority var (ana sayfa daily/0.8, yazılar monthly/0.7). Google bunları yok sayıyor; zararı yok, faydası da yok.

İkincisi, lastmod'un doğruluğu CMS'teki güncelleme tarihinin doğruluğuna bağlı. CMS bir toplu içe aktarma veya şema değişikliği sırasında bütün yazıların güncelleme tarihini aynı güne çekerse sitemap de bütün yazılar için aynı günü bildirir. Seri planlanırken 7 Kasım'da alınan bir örnekte lastmod değerlerinin bütün sayfalarda aynı gün olduğu not edilmişti; böyle bir durum, yukarıda anlattığım güven kaybının tetikleyicisi olabilir. Yazının içeriği değişmediyse güncelleme tarihinin değişmemesi gerekir.

Üçüncüsü, sitemap action'ı yazıları CMS'ten en fazla 500 kayıtla çekiyor (GetPostsAsync("tr", 1, 500, ...)). Yayımlanan yazı sayısı 500'ü geçtiği gün en eski yazılar sitemap'ten sessizce düşer; hata yok, uyarı yok. Bu yazıdaki parçalı sitemap yapısı tam bu sınırın çözümü. Değişiklik kararı site sahibinin.

Üretimde Dikkat

Sitemap'e yalnız canonical, 200 dönen, index'lenmesini istediğin URL'leri koy. Redirect eden, noindex'li veya canonical'ı başka yeri gösteren bir URL'yi sitemap'te listelemek, Google'a çelişkili sinyal göndermektir; Search Console'un sitemap raporu bu URL'leri ayrıca işaretler.

Sitemap'i hem robots.txt'deki Sitemap satırıyla hem Search Console'dan bildir. Search Console'daki sitemap raporu Google'ın dosyayı en son ne zaman okuduğunu, kaç URL bulduğunu ve hataları gösteriyor; parçalı sitemap'te her parçayı ayrı izleyebilirsin.

Sitemap endpoint'inin performansını izle. Googlebot ve diğer crawler'lar sitemap'i düzenli olarak ister; cache'siz ve 0,35 saniye süren bir sorgu, birkaç parça ve birkaç crawler ile veritabanında fark edilir bir yük olur. Output cache ve tag'li invalidation bunu çözüyor.

Taslak ve yayın tarihi gelecekte olan içeriği filtrele. Where(b => b.IsPublished) yetmiyorsa (planlanmış yayınlar varsa) PublishedAt <= now koşulunu da ekle; sitemap'te yayında olmayan bir URL, 404 dönen bir crawl isteği demek.

Sık Düşülen Tuzaklar

lastmod'a üretim zamanını yazmak. Her URL'nin her gün değiştiğini söyleyen bir sitemap, değişiklik sinyalini tamamen değersizleştirir.

changefreq ve priority ile crawl'ı yönetmeye çalışmak. Google ikisini de yok sayıyor.

Göreli URL yazmak veya sitemap'i alt klasöre koymak. <loc>/books/dune</loc> geçersiz; /sitemaps/ altındaki bir dosya Search Console'dan gönderilmediyse yalnız o klasörü kapsar.

50.000 URL sınırını sayıp 50 MB'ı unutmak, ya da tersi. İkisi birlikte geçerli; hreflang'lı sitemap'lerde boyut beklenenden hızlı büyür.

IndexNow'u Google için kurmak. Google katılımcı değil; IndexNow Bing, Yandex ve diğer katılımcılar için.

Küçük Bir Deneme

★☆☆ Kendi sitenin sitemap'ini indir ve şu üç şeyi say: kaç URL var, kaç farklı lastmod değeri var, en sık görülen lastmod değeri kaç URL'de geçiyor? Tek bir tarih URL'lerin çoğunda tekrar ediyorsa nedenini bul.

★★☆ WriteBooksAsync'i Skip/Take yerine Id aralığıyla bölünecek şekilde değiştir, ardından bir kitap yayımlayıp üç parçanın boyutunu önce ve sonra karşılaştır. Kaç dosya değişti?

★★★ IndexNow çağrısını yayın endpoint'inden çıkar: yayın, değişen URL'yi bir outbox tablosuna yazsın; bir BackgroundService her dakika outbox'taki URL'leri 10.000'lik batch'ler hâlinde göndersin, 429 aldığında geri çekilsin ve başarılı gönderimleri işaretlesin. Outbox'ın neden gerekli olduğunu (yayın kaydedildi ama bildirim gönderilemedi senaryosu) bir test ile göster.

Sitemap, Google'a hangi URL'yi istediğini söylüyor; ama aynı içerik birden fazla URL'de duruyorsa Google hangisini seçecek? Bir sonraki yazıda duplicate content'in kaynaklarını, rel=canonical'ı ve diğer canonical sinyallerini, ASP.NET Core'da bir canonical tag helper'ı ve Angular'da SSR sırasında canonical link'ini kodla göreceğiz.