llms.txt ve Markdown Sürümleri: Spec, Kim Kullanıyor ve ASP.NET Core'da Uygulama
llms.txt, sitenin kökünde H1, özet ve link listelerinden oluşan bir Markdown haritasıdır; Google Search onu yok sayar, büyük AI arama motorlarının kullandığına dair resmi açıklama yoktur, asıl kullanım alanı kod asistanları ve agent'lardır. Spec v2'yi, EF Core verisinden llms.txt ve yazı başına Markdown üreten Minimal API'yi, Output Cache invalidation'ı ve mertozen.com'un dosyasını inceliyoruz.
Sürüm notu: Bu yazı llmstxt.org'daki llms.txt önerisinin v2 hâlini (Ağustos 2026) esas alır; kod örnekleri .NET 10, EF (Entity Framework) Core 10.0.14, ReverseMarkdown 6.3.0 ve Lighthouse 13.6.0 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore blogu için bir /llms.txt yayımlamaya karar verdin. Üç sorunun var. Birincisi, bu dosya Google'ın AI (Artificial Intelligence, yapay zekâ) destekli sonuçlarına, yani AI Overviews'a girmene yardım eder mi? İkincisi, llms.txt'teki link'ler HTML (HyperText Markup Language) sayfalarına mı, yoksa başka bir şeye mi işaret etmeli? Üçüncüsü, bir yazıyı Markdown'a çevirdiğinde içindeki <pre><code> bloğu ve /books/async-in-practice gibi göreli bir link ne olur?
llms.txt Nedir ve Ne Değildir?
llms.txt, Jeremy Howard'ın 3 Eylül 2024'te önerdiği bir format: sitenin köküne konan, LLM'lerin (Large Language Model) ve onları kullanan araçların siteyi hızlıca anlaması için yazılmış bir Markdown dosyası. Önerinin sayfası en son 10 Ağustos 2026'da güncellendi ve kendini v2 olarak tanımlıyor. Bir standart kuruluşunun onayladığı bir spec değil, bir topluluk önerisi.
robots.txt ile karıştırılmaması gerekiyor. robots.txt bot'lara neyi crawl edebileceklerini söyler; llms.txt hiçbir izin veya yasak taşımaz. Bir içerik haritası ve özet. "AI eğitiminde kullanılmayayım" kararının yeri serinin üçüncü yazısında gördüğümüz robots.txt token'ları, llms.txt değil.
Spec v2: Yapı ve Keşif
Dosyanın yapısı sıralı ve kısa. İsteğe bağlı bir BOM (Byte Order Mark), ardından projenin veya sitenin adıyla bir H1 (birinci düzey başlık, Markdown'da #); bu, dosyadaki tek zorunlu öğe. Sonra kısa bir özet içeren bir blockquote, başlık içermeyen sıfır veya daha fazla açıklama bölümü ve dosya listeleri içeren sıfır veya daha fazla H2 (ikinci düzey başlık, ##) bölümü. Listedeki her madde zorunlu bir [ad](url) link'i ve isteğe bağlı olarak : ile başlayan notlar taşıyor. "Optional" adlı H2'nin özel bir anlamı var: kısa bir context gerektiğinde atlanabilecek ikincil link'ler.
Öneri ayrıca her sayfanın aynı URL'de (Uniform Resource Locator) bir Markdown sürümü olmasını öneriyor: page.html.md veya page.md; dosya adı olmayan URL'ler için index.html.md. Dolayısıyla baştaki ikinci sorunun cevabı: llms.txt'teki link'lerin Markdown sürümlerine gitmesi spec'in amacına daha uygun.
v2'de keşif için iki ilişki tanımlı: rel="alternate" ve type="text/markdown" sayfanın Markdown sürümünü, rel="describedby" ise o sayfayı kapsayan llms.txt dosyasını gösteriyor. İkisi de HTML <link> element'i veya HTTP (Hypertext Transfer Protocol) Link header'ı olarak verilebiliyor; header yöntemi HTML olmayan dosyalarda da çalışıyor ve sunucu veya CDN (Content Delivery Network) seviyesinde ayarlanabiliyor. Bir llms.txt, bulunduğu path'in altındaki bütün sayfaları kapsıyor.
Dürüst Durum: Kim Kullanıyor?
Google tarafında cevap net. Google'ın AI optimization rehberi, Google Search'te görünmek için yeni makine okunabilir dosyalar, AI metin dosyaları veya Markdown gerekmediğini yazıyor; 15 Haziran 2026'da eklenen not llms.txt oluşturmanın "tamamen sorun olmadığını", ama Google Search bu dosyaları yok saydığı için görünürlüğe ne yardım ne zarar edeceğini söylüyor. Baştaki ilk sorunun cevabı bu: AI Overviews'a girmene yardım etmez.
Diğer büyük AI arama motorları için Kasım 2026 itibarıyla OpenAI'ın, Anthropic'in veya Perplexity'nin crawler dokümanlarında llms.txt'in kullanıldığına dair resmi bir ifade bulamadım. Server log'unda bir bot'un /llms.txt'i istemesi de dosyanın bir cevapta kullanıldığını kanıtlamaz; yalnızca indirildiğini gösterir.
Formatın gerçek kullanım alanı başka: geliştirici dokümantasyonu, kod asistanları ve agent'lar. Bir kod asistanına bir kütüphanenin llms.txt'ini vermek, ona HTML'i ayıklamakla uğraşmadan temiz bir içerik haritası sunuyor. Bir de Google'ın kendi aracı var: Lighthouse 13.6.0'daki Agentic Browsing kategorisinin içinde bir llms-txt audit'i bulunuyor. Audit'in kaynak koduna göre dosya 4xx dönerse audit "uygulanamaz" sayılıyor, 5xx veya 429 dönerse başarısız oluyor; dosya varsa en az bir H1, en az bir Markdown link'i ve en az 50 karakter arıyor. Bu kategorinin bütününü serinin altıncı yazısında açacağız.
ASP.NET Core'da llms.txt Endpoint'i
Bookstore blogunun yazıları EF Core ile bir veritabanında. llms.txt'i elle yazmak yerine aynı veriden üretmek, bakımın kendiliğinden yapılması demek:
app.MapGet("/llms.txt", async (BlogDbContext db, CancellationToken ct) =>
{
List<BlogPost> posts = await db.Posts.AsNoTracking()
.OrderBy(p => p.Series).ThenByDescending(p => p.PublishedAt)
.ToListAsync(ct);
StringBuilder sb = new();
sb.AppendLine("# Bookstore Blog");
sb.AppendLine();
sb.AppendLine("> Hands-on posts about the books we sell: tested code, measured results, dated version notes.");
foreach (IGrouping<string, BlogPost> series in posts.GroupBy(p => p.Series))
{
sb.AppendLine();
sb.AppendLine($"## {series.Key}");
sb.AppendLine();
foreach (BlogPost post in series)
{
sb.AppendLine($"- [{post.Title}]({SiteUrl}/blog/{post.Slug}.md): {post.Summary}");
}
}
sb.AppendLine();
sb.AppendLine("## Optional");
sb.AppendLine();
sb.AppendLine($"- [Book catalog]({SiteUrl}/): every book we sell, as HTML");
return Results.Text(sb.ToString(), "text/plain; charset=utf-8");
}).CacheOutput(policy => policy.Tag("blog"));
Gerçek çıktı:
# Bookstore Blog
> Hands-on posts about the books we sell: tested code, measured results, dated version notes.
## Book Reviews
- [Review: Testing Without Fear](https://bookstore.example/blog/testing-without-fear-review.md): What the book gets right about contract tests, and the two chapters we skipped.
- [Review: Modeling Bounded Contexts](https://bookstore.example/blog/modeling-bounded-contexts-review.md): A practical book on splitting a domain, read by a team that actually split one.
## Field Notes
- [Backpressure in Our Order Service](https://bookstore.example/blog/backpressure-in-our-order-service.md): We applied the bounded channel example from Async in Practice to our order service.
## Optional
- [Book catalog](https://bookstore.example/): every book we sell, as HTML
Yazılar seriye göre H2'lerde gruplanıyor, link'ler Markdown sürümlerine gidiyor, ikincil içerik "Optional" altında. Content type olarak text/plain seçtim; tarayıcıda okunabilir kalıyor ve dosya zaten düz metin olarak da geçerli Markdown.
Yazı Başına Markdown Endpoint'i
Markdown sürümü için HTML gövdeyi çeviren bir kütüphane gerekiyor; ReverseMarkdown 6.3.0 kullandım. ASP.NET Core routing'i {slug}.md gibi bir complex segment'i destekliyor, bu yüzden aynı slug'ın HTML ve Markdown sürümü yan yana duruyor:
private static readonly Converter HtmlToMarkdown = new(new Config
{
GithubFlavored = true,
Tags = { Unknown = Config.UnknownTagsOption.Bypass },
});
app.MapGet("/blog/{slug}.md", async (string slug, BlogDbContext db, CancellationToken ct) =>
{
BlogPost? post = await db.Posts.AsNoTracking().FirstOrDefaultAsync(p => p.Slug == slug, ct);
if (post is null)
{
return Results.NotFound();
}
DateTime lastChange = post.UpdatedAt ?? post.PublishedAt;
// göreli link'ler site dışında anlamsız, önce mutlak hâle getir
string html = post.BodyHtml.Replace("href=\"/", $"href=\"{SiteUrl}/");
string markdown = $"""
# {post.Title}
> {post.Summary}
Published: {post.PublishedAt:yyyy-MM-dd} · Updated: {lastChange:yyyy-MM-dd} · Canonical: {SiteUrl}/blog/{post.Slug}
{HtmlToMarkdown.Convert(html)}
""";
return Results.Text(markdown, "text/markdown; charset=utf-8");
}).CacheOutput(policy => policy.Tag("blog"));
Yazının HTML gövdesinde class="language-csharp" taşıyan bir <pre><code> bloğu, içinde <Order> gibi encode edilmiş karakterler ve göreli bir link var. Gerçek Markdown çıktısı:
# Backpressure in Our Order Service
> We applied the bounded channel example from Async in Practice to our order service.
Published: 2026-10-02 · Updated: 2026-10-20 · Canonical: https://bookstore.example/blog/backpressure-in-our-order-service
## The bounded channel
We capped the queue at `100` items so producers wait instead of piling up orders:
```csharp
Channel<Order> orders = Channel.CreateBounded<Order>(
new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.Wait });
```
Read the [book page](https://bookstore.example/books/async-in-practice) for the full chapter list.
Baştaki üçüncü sorunun cevabı: GithubFlavored ayarıyla kod bloğu dil bilgisiyle birlikte fenced code block'a dönüşüyor, encode edilmiş < ve > karakterleri geri açılıyor, girinti korunuyor. Göreli link ise kendiliğinden mutlak olmuyor; ilk denememde (/books/async-in-practice) olarak kaldı. Site dışında okunacak bir dosyada göreli link işe yaramadığı için çeviriden önce mutlak hâle getirdim. Tarih ve canonical satırı da bilinçli: Markdown sürümü, okurun ve bir aracın içeriğin ne kadar güncel olduğunu ve asıl adresini görebileceği tek yer.
Keşif: Link Header'ı ve Output Cache
HTML sayfa, Markdown sürümünü ve llms.txt'i v2'deki ilişkilerle bildiriyor:
response.Headers.Link =
$"</blog/{slug}.md>; rel=\"alternate\"; type=\"text/markdown\", </llms.txt>; rel=\"describedby\"";
$ curl -si http://localhost:5481/blog/backpressure-in-our-order-service | grep -i "^link"
Link: </blog/backpressure-in-our-order-service.md>; rel="alternate"; type="text/markdown", </llms.txt>; rel="describedby"
Her iki endpoint de "blog" tag'iyle Output Cache'te. Output Cache'in mekaniğini ASP.NET Core Web API (Application Programming Interface) serisinin ".NET 7 ile ASP.NET Core Yenilikleri" yazısında görmüştük; burada önemli olan invalidation. CMS (Content Management System) bir yazı yayımladığında veya düzenlediğinde tag'i boşaltan bir endpoint çağırıyor:
// CMS bir yazı yayımladığında veya düzenlediğinde çağırır
app.MapPost("/internal/blog-cache/evict", async (IOutputCacheStore cache, CancellationToken ct) =>
{
await cache.EvictByTagAsync("blog", ct);
return Results.NoContent();
});
$ curl -s -D - -o /dev/null http://localhost:5481/llms.txt
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
$ curl -s -D - -o /dev/null http://localhost:5481/llms.txt
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Age: 2
$ curl -s -o /dev/null -w "evict: %{http_code}\n" -X POST http://localhost:5481/internal/blog-cache/evict
evict: 204
$ curl -s -D - -o /dev/null http://localhost:5481/llms.txt
HTTP/1.1 200 OK
İkinci istekte Age: 2 header'ı cache'ten geldiğini gösteriyor; evict'ten sonraki istek yeniden üretiliyor. Gerçek bir uygulamada bu internal endpoint authentication arkasında olmalı; burada kısalık için açık bıraktım.
Son kontrol olarak Lighthouse 13.6.0'ı yerel sayfaya karşı yalnızca bu audit ile çalıştırdım: npx lighthouse http://localhost:5481/blog/backpressure-in-our-order-service --only-audits=llms-txt. Sonuç: llms-txt, "llms.txt follows recommendations", skor 1.
mertozen.com'un llms.txt'i: Ne Yaptık, Ne Ölçemiyoruz?
Bu site zaten /llms.txt ve /llms-full.txt yayımlıyor; ikisi de SeoController'da CMS verisinden üretiliyor ve bir saat cache'leniyor. Kaynak koda göre llms.txt'te bir H1, bir blockquote özet, en yeni 10 yazı, en yeni 5 video, Hakkımda ve İletişim sayfaları ve geliştirici araçları listesi var. llms-full.txt ise bütün yazıları kategori ve seri hiyerarşisinde, okuma sırasıyla listeliyor. Yapı spec'e uygun ve Lighthouse'un aradığı üç şartı karşılıyor.
Spec v2'ye göre geliştirilebilecek yerler de var. Link'ler HTML sayfalarına gidiyor; yazıların Markdown sürümü yok. Yalnızca TR adresler listeleniyor; /en/ altındaki İngilizce yazılar için ayrı bir bölüm veya ayrı bir /en/llms.txt yok. Sayfaların head'inde <link rel="llms-txt"> kullanılıyor; bu değer spec'te tanımlı değil, v2'nin karşılığı rel="describedby". Aynı yerde rel="alternate" type="text/markdown" llms-full.txt'e işaret ediyor; spec'te bu ilişki sayfanın kendi Markdown sürümü için tanımlı, site geneli bir dosya için değil.
Ve dürüst kısım: bu dosyaların sitenin herhangi bir AI cevabında görünmesine katkısını ölçemiyoruz. Google Search kullanmadığını söylüyor, diğer büyük motorlar kullandığını söylemiyor; log'da indirilmesi kullanımı kanıtlamıyor. Dosyanın değeri bugün için kod asistanları ve agent'lar tarafında, ve o kullanım da ölçülebilir bir trafik bırakmıyor.
Üretimde Dikkat
Bakımı otomatikleştir ya da hiç başlama. Elle güncellenen bir llms.txt birkaç ay içinde eskir; içerikle aynı veriden, aynı deploy'da üretilmeli.
Markdown sürümlerini duplicate içerik riskine karşı düşün. Google'ın notu, Google'ın birçok dosya tipini crawl edebildiğini ama bunları özel muamele görmüş saymadığını söylüyor. Markdown endpoint'ine canonical bilgisini koymak ve gerekiyorsa X-Robots-Tag: noindex eklemek, bu sürümlerin arama sonuçlarında HTML sayfayla yarışmasını önler; ayrıntısı Teknik SEO (Search Engine Optimization) serisinin "noindex ve X-Robots-Tag" yazısında.
Dosyayı kısa ve önem sırasına göre tut. Spec'in "Optional" bölümü tam bunun için var: bir araç context'i daraltmak zorunda kaldığında ilk atılacak link'ler orada durmalı. Bütün arşivi tek dosyaya dökmek yerine en önemli serileri ve sayfaları öne al.
Ne zaman uğraşmamalı: içerik sitesiysen ve maliyetini ölçemiyorsan, llms.txt'in önceliği Search Console'daki indexing sorunlarının, yavaş sayfaların ve eksik içerik kalitesinin gerisinde.
Sık Düşülen Tuzaklar
llms.txt'i robots.txt'in alternatifi sanmak. İzin veya yasak taşımaz.
llms.txt'i bir Google sıralama faktörü sanmak. Google yok saydığını açıkça yazıyor.
Markdown çevirisinde kod bloklarını ve link'leri test etmemek. Encode edilmiş karakterler, dil bilgisi ve göreli link'ler en sık bozulan yerler.
Bütün siteyi tek bir dev dosyaya dökmek. Spec kısa bir harita ve "Optional" ile kısaltılabilir bir yapı öneriyor; tam metin için sayfa başına Markdown sürümleri var.
Küçük Bir Deneme
★☆☆ Kendi sitende llms.txt varsa H1, blockquote ve link'lerin spec'e uyup uymadığını kontrol et; yoksa üç satırlık bir taslak yaz.
★★☆ Markdown endpoint'ine bir HTML tablo içeren yazı ekle ve ReverseMarkdown'ın tabloyu nasıl çevirdiğine bak. Başlık satırı olmayan bir tablo ne oluyor?
★★★ Bookstore'a dil desteği ekle: /en/llms.txt İngilizce yazıları, /llms.txt Türkçe yazıları listelesin; her HTML sayfanın Link header'ı kendi dilindeki llms.txt'i describedby olarak göstersin. İki dili de curl ile doğrula.
Bir sonraki yazıda llms.txt'in de içinde bulunduğu daha büyük resme bakıyoruz: sayfanı okuyan bir browser agent ne görüyor, accessibility tree neden önemli ve Lighthouse'un Agentic Browsing kategorisi neyi kontrol ediyor.