Semantic HTML ve Landmark'lar: header, nav, main, article, section, aside, footer ve search
Semantic element'ler accessibility tree'de landmark'a dönüşür; ama adsız section region olmaz, article içindeki header banner olmaz ve section içindeki h1 hâlâ birinci seviyedir. Bu yazıda Bookstore'un kitap detay sayfasını div soup'tan semantic yapıya taşıyor, her adımı Chromium'un accessibility tree'sinden okuyor, section/article/div kararını, search element'ini, tek main kuralını ve outline algoritması olmadan heading hiyerarşisini kuruyoruz.
Sürüm notu: Bu yazı HTML (HyperText Markup Language) Living Standard'ın ve Baseline'ın Kasım 2026 durumuna göre yazıldı; örnekler Chrome 158, WebKit 27.2 (Playwright 1.65) ve Nu Html Checker 26.11.9 ile denenmiştir.
Önce Tahmin Et
Bookstore'un kitap detay sayfası yıllardır şöyle yazılmış: en dışta div.header, ortada div.content, içinde div.book ve div.sidebar, en altta div.footer. Sayfa Nu Html Checker'dan tek bir hata almadan geçiyor. İki soru: bir ekran okuyucu kullanıcısı "landmark'lar arasında gez" kısayoluna bastığında bu sayfada kaç landmark bulur? Ve sayfayı semantic element'lerle yeniden yazdığımızda, kitabın kendi <article>'ının içine koyduğumuz <header> da sayfanın üst bandı gibi bir "banner" landmark'ı olur mu?
İlk yazıda HTML'in nasıl parse edildiğini ve geçerli markup'ın neden önemli olduğunu gördük. Bu yazıda bir adım öteye geçiyoruz: geçerli olmak yetmiyor, markup'ın bir de anlam taşıması lazım. Cevapları tahminle değil, Chromium'un accessibility tree'sinden okuyarak vereceğiz.
Div Soup: Geçerli Ama Anlamsız
Başlangıç noktası, sınıf adlarından başka hiçbir anlam taşımayan sayfa:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Clean Architecture | Bookstore</title>
</head>
<body>
<div class="header">
<div class="logo">Bookstore</div>
<div class="menu">
<a href="/books">Books</a>
<a href="/authors">Authors</a>
<a href="/cart">Cart</a>
</div>
<div class="search-box">
<form action="/search">
<input type="search" name="q" aria-label="Search books">
<button>Search</button>
</form>
</div>
</div>
<div class="content">
<div class="book">
<div class="title">Clean Architecture</div>
<div class="author">Robert C. Martin</div>
<div class="section-title">Description</div>
<div class="text">A guide to software structure and design.</div>
<div class="section-title">Reviews</div>
<div class="review">
<div class="review-title">Worth reading twice</div>
<div class="text">The chapters on boundaries changed how I split modules.</div>
</div>
</div>
<div class="sidebar">
<div class="section-title">Customers also bought</div>
<a href="/books/refactoring">Refactoring</a>
</div>
</div>
<div class="footer">© 2026 Bookstore</div>
</body>
</html>
CSS (Cascading Style Sheets) ile bu sayfayı göz için mükemmel hâle getirebilirsin. Ama ekran okuyucu, tarayıcı eklentileri, otomasyon araçları ve AI (Artificial Intelligence) agent'ları sayfayı gözle değil, tarayıcının DOM'dan (Document Object Model) türettiği accessibility tree üzerinden okur. Bu ağacı görmek için Chromium'un CDP (Chrome DevTools Protocol) arayüzünden tam ağacı isteyen ve yalnız landmark ile heading node'larını yazdıran küçük bir script yazdım:
const { chromium } = require('playwright');
const fs = require('fs');
const source = fs.readFileSync(process.argv[2], 'utf8');
const show = new Set(['banner', 'navigation', 'search', 'main', 'article', 'region',
'complementary', 'contentinfo', 'heading', 'sectionheader', 'sectionfooter']);
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(source);
const cdp = await page.context().newCDPSession(page);
// Tarayıcının hesapladığı accessibility tree'nin tamamı
const { nodes } = await cdp.send('Accessibility.getFullAXTree');
const byId = new Map(nodes.map(n => [n.nodeId, n]));
const walk = (n, depth) => {
const role = n.role?.value;
const visible = !n.ignored && show.has(role);
if (visible) {
const level = n.properties?.find(p => p.name === 'level');
const label = n.name?.value ? ` "${n.name.value}"` : '';
console.log(`${' '.repeat(depth)}${role}${label}${level ? ' level=' + level.value.value : ''}`);
}
for (const id of n.childIds ?? []) walk(byId.get(id), visible ? depth + 1 : depth);
};
walk(nodes[0], 0);
await browser.close();
})();
Div soup sayfası için node axtree.js soup.html hiçbir satır yazdırmıyor. Sıfır landmark, sıfır heading. "Clean Architecture" başlığı da "Reviews" başlığı da ekran okuyucu için sıradan metin. Kullanıcı ana içeriğe atlayamıyor, heading'ler arasında gezemiyor; sayfayı baştan sona dinlemek zorunda. Tahmin sorusunun ilk cevabı bu: sıfır.
Semantic Yapıya Refactor
Aynı içerik, bu kez her parçayı anlamına göre seçilmiş bir element'le:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Clean Architecture | Bookstore</title>
</head>
<body>
<header>
<a href="/">Bookstore</a>
<nav aria-label="Main">
<ul>
<li><a href="/books">Books</a></li>
<li><a href="/authors">Authors</a></li>
<li><a href="/cart">Cart</a></li>
</ul>
</nav>
<search>
<form action="/search">
<label for="q">Search books</label>
<input type="search" id="q" name="q">
<button>Search</button>
</form>
</search>
</header>
<main>
<article>
<header>
<h1>Clean Architecture</h1>
<p>Robert C. Martin</p>
</header>
<section aria-labelledby="description">
<h2 id="description">Description</h2>
<p>A guide to software structure and design.</p>
</section>
<section aria-labelledby="reviews">
<h2 id="reviews">Reviews</h2>
<article>
<h3>Worth reading twice</h3>
<p>The chapters on boundaries changed how I split modules.</p>
</article>
</section>
</article>
<aside aria-labelledby="also-bought">
<h2 id="also-bought">Customers also bought</h2>
<a href="/books/refactoring">Refactoring</a>
</aside>
</main>
<footer>
<p>© 2026 Bookstore</p>
</footer>
</body>
</html>
> node axtree.js semantic.html
banner
navigation "Main"
search
main
article
sectionheader
heading "Clean Architecture" level=1
region "Description"
heading "Description" level=2
region "Reviews"
heading "Reviews" level=2
article
heading "Worth reading twice" level=3
complementary "Customers also bought"
heading "Customers also bought" level=2
contentinfo

Artık ağacın bir iskeleti var. Sayfa düzeyindeki header banner oldu, nav navigation, search search, main main, aside complementary, sayfa düzeyindeki footer da contentinfo. Bu altı role, ARIA'nın (Accessible Rich Internet Applications) landmark role'leri; ekran okuyucular bunları ayrı bir listede gösterir ve tek tuşla aralarında gezdirir. Hiçbir yere role attribute'u yazmadım: bu eşlemeler element'lerin implicit (örtük) role'leri ve W3C'nin (World Wide Web Consortium) ARIA in HTML belgesi bunları tek tek listeler.
Tahmin sorusunun ikinci cevabı da çıktıda: kitabın article'ı içindeki header banner değil, sectionheader oldu. Chromium 158 burada ARIA 1.3'ün yeni sectionheader role'ünü kullanıyor; bu role landmark değil. Sayfada tek bir banner var, olması gereken de bu.
Eşlemeyi gerçekten yapan şey sadece element seçimi değil. nav'a aria-label="Main" verdim, çünkü sayfada ikinci bir nav (ör. footer'da yasal linkler) olursa kullanıcı ikisini adından ayırt etmeli. section ve aside'ı da aria-labelledby ile kendi heading'lerine bağladım; neden gerektiğini bir sonraki bölümde göreceğiz.
Element'ten Landmark'a: Ne Zaman Olur, Ne Zaman Olmaz
Semantic element'lerin hepsi her koşulda landmark değildir. Kuralları görmek için kasıtlı olarak "yarım" yazılmış bir sayfa hazırladım (doctype ve head önceki sayfadakiyle aynı, burada yalnız main):
<main>
<section>
<h1>Shipping</h1>
<p>Free over 50 USD.</p>
</section>
<article>
<h1>Clean Architecture</h1>
<aside>
<p>Also available as an e-book.</p>
</aside>
<footer>
<p>Published 2017</p>
</footer>
</article>
<aside>
<p>Gift wrapping available.</p>
</aside>
</main>
> node axtree.js variants.html
main
heading "Shipping" level=1
article
heading "Clean Architecture" level=1
sectionfooter
complementary
Dört ders var. Birincisi, adı olmayan section ağaçta görünmüyor bile; role'ü generic, yani bir div'den farkı yok. section ancak accessible name (erişilebilir ad) aldığında region landmark'ı olur; en temiz yol aria-labelledby ile kendi heading'ine bağlamak. İkincisi, article içindeki adsız aside da kayboldu: sectioning content (article, section, nav, aside) içindeki aside ancak adı varsa complementary olur. main'in doğrudan içindeki aside ise adsız da olsa complementary. Üçüncüsü, article içindeki footer contentinfo değil, sectionfooter. Aynı kural main içindeki header ve footer için de geçerli; Angular'ın <app-root>'u gibi bir custom element ise bu kuralı etkilemiyor, onun içindeki sayfa düzeyi header yine banner. Dördüncüsü, section içindeki h1 hâlâ level=1; buna heading bölümünde döneceğiz.
Pratik sonuç: header ve footer'ı gönül rahatlığıyla her article içinde kullanabilirsin, sayfa düzeyindeki landmark'larla çakışmazlar. section yazıyorsan ona bir heading ver ve aria-labelledby ile bağla; veremiyorsan büyük ihtimalle div istiyorsun.
section mı, article mı, div mi?
En çok tartışılan üçlü bu. Spec'in tanımlarını kendi cümlelerimle toparlayıp Bookstore'dan örneklerle bir karar listesine çeviriyorum:
- Parça tek başına anlamlı mı, başka bir sayfaya ya da feed'e taşınsa bile anlaşılır mı? Kitap kartı, kullanıcı yorumu, blog yazısı, sipariş özeti:
article. - Parça daha büyük bir bütünün kendi başlığı olan bir bölümü mü? Kitap detayında "Description", "Reviews", "Table of contents":
sectionve içinde bir heading. - Parça yalnız stil, layout ya da JavaScript için bir kutu mu? Grid sarmalayıcısı, kart gölgesi için iç kutu:
div. - Parça ana içerikle ilişkili ama ondan ayrılabilir mi? "Customers also bought", yazar hakkında kısa kutu:
aside. - Parça bir link grubu ama ana navigasyon değil mi? Footer'daki yasal linkler: ayrı bir adla ikinci bir
nav; tek bir "Read more" linki içinnavgerekmez.
İlk iki maddeyi karıştırmamak için kullandığım test basit: içeriği kopyalayıp başka bir sayfaya yapıştırdığında hâlâ anlamlıysa article, bulunduğu sayfanın bağlamı olmadan yarım kalıyorsa section. Bir yorum, kitabın sayfasından çıkarıldığında da yorumdur; "Description" başlıklı bölüm, hangi kitabın açıklaması olduğunu bilmeden anlamsız. Bu yüzden yukarıdaki sayfada kitap bir article, açıklaması bir section, yorum da yine bir article. İç içe article tamamen geçerli; spec'in kendi örneklerinde de yorumlar yazının içinde birer article.
Nu Html Checker da bu kararları kontrol etmene yardım ediyor. Heading'i olmayan bir section ve article verdiğimde gelen çıktı:
:4.7-4.15: info warning: Section lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all sections, or else use a “div” element instead for any cases where no heading is needed.
:4.46-4.54: info warning: Article lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all articles.
Hata değil, uyarı; ama mesaj tam olarak yukarıdaki kuralı söylüyor: heading'i olmayan bir bölüm büyük ihtimalle div olmalı.
search Element'i ve Tek main Kuralı
Listedeki en yeni element <search>. Eskiden arama formunu <form role="search"> ya da <div role="search"> ile işaretlerdik; artık bunun için bir element var ve implicit role'ü doğrudan search. Safari 17, Firefox 118 ve Chrome 118 ile Ekim 2023'te Baseline newly available oldu, Nisan 2026'da widely available'a geçti; yani bugün fallback düşünmeden kullanabilirsin. Önemli bir ayrıntı: search form'un yerini almaz, onu sarar. Gönderme işi hâlâ içindeki form'un. Ayrıca yalnız arama ve filtreleme için kullanılır, arama sonuçlarının kendisi için değil; Bookstore'un katalog sayfasındaki filtre paneli search, altındaki sonuç listesi değil.
main için kural daha katı: bir document'ta görünen tek bir main olabilir. İki tane yazdığında checker bunu hata olarak işaretliyor:
:5.1-5.6: error: A document must not include more than one visible “main” element.
Aynı dosyadaki üçüncü <main hidden> için hata gelmedi: spec, hidden attribute'u olan main'leri bu sayımdan çıkarıyor. Bu, tek sayfa uygulamalarında (SPA, Single-Page Application) birden fazla görünümü DOM'da tutup yalnız birini gösteren yapılar için konmuş bir istisna. Angular'da router zaten tek bir görünümü render ettiği için main'i <router-outlet>'u saran layout component'ine bir kez yazmak yeterli; ASP.NET Core'da da yeri _Layout.cshtml'deki @RenderBody()'nin etrafı. Her view'a ayrıca main yazan ekiplerde iki main hatası tam buradan çıkar.
Heading Hiyerarşisi: Document Outline Hiç Gelmedi
HTML5 (HTML'in beşinci nesli) döneminin en ısrarlı efsanesi document outline algoritması. Fikir şuydu: her section ve article kendi heading seviyesini sıfırlar, sen her bölümde gönül rahatlığıyla h1 yazarsın, tarayıcı iç içe geçişe bakıp gerçek seviyeyi hesaplar. Birçok eğitim, hatta bir dönem spec'in kendi örnekleri bu yaklaşımı önerdi.
Sorun şu ki hiçbir tarayıcı ve hiçbir ekran okuyucu bu algoritmayı uygulamadı. On yıldan uzun süren bu durum sonunda spec'e yansıdı: WHATWG (Web Hypertext Application Technology Working Group) Temmuz 2022'de birleştirilen bir değişiklikle sectioning'e dayalı outline algoritmasını kaldırdı ve yerine yalnız heading seviyelerine bakan basit bir tanım koydu. Yukarıdaki variants.html çıktısı bunun bugünkü hâlini gösteriyor: section içindeki h1 de, article içindeki h1 de Chromium için level=1. Ekran okuyucu kullanıcısının heading listesinde sayfada üç tane birinci seviye başlık var ve hangisinin asıl başlık olduğunu anlamanın yolu yok.
Çözüm sıkıcı ama güvenilir: seviyeleri elle doğru kur. Sayfada bir tane h1 olsun ve sayfanın konusunu söylesin; kitap detayında kitabın adı. Bölümler h2, onların alt bölümleri h3. Seviye atlamaktan kaçın: h2'den sonra doğrudan h4'e inmek, ekran okuyucu kullanıcısına arada bir bölümü kaçırdığı izlenimini verir. Heading'i görsel boyut için seçme; küçük görünen bir başlık gerekiyorsa doğru seviyeyi seç ve CSS ile küçült. Spec'e headingoffset adında, bir alt ağaçtaki heading seviyelerini kaydıran yeni bir attribute eklenmesi üzerinde çalışılıyor; ama Kasım 2026 itibarıyla Baseline'dan çok uzak, Chrome 158'de yok. Bugün tek güvenilir yöntem seviyeleri elle yazmak.
Bu kural component'lerle çalışırken zorlaşıyor: aynı kitap kartı component'i ana sayfada bir h2'nin altında, arama sonuçlarında bir h1'in altında duruyorsa kartın başlığı hangi seviye olmalı? Pratik çözüm seviyeyi component'e input olarak vermek ve varsayılanını sayfadaki en yaygın kullanıma göre seçmek. Angular'da bunu bir input ile, Razor'da bir partial parametresiyle yapmak birkaç satır.
Neden Önemli: Ekran Okuyucu, Testler, Arama Motorları ve AI Agent'lar
Semantic HTML'in ilk faydalanıcısı ekran okuyucu kullanıcıları: landmark ve heading listeleri onların sayfadaki "içindekiler"idir. ARIA'nın, klavye etkileşiminin ve ekran okuyucu davranışının derinliği Accessibility (a11y) serisinde; burada bilmen gereken, doğru element seçiminin bu işin büyük kısmını bedavaya yaptığı.
İkinci faydalanıcı senin testlerin. Playwright ve Testing Library gibi araçlar element'leri CSS class'larıyla değil, accessibility tree'deki role ve adlarıyla bulmayı önerir. Semantic sayfada bu sorgular doğal olarak yazılıyor:
const { chromium } = require('playwright');
const fs = require('fs');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(fs.readFileSync('semantic.html', 'utf8'));
const reviews = page.getByRole('region', { name: 'Reviews' });
console.log('review headings:', await reviews.getByRole('heading', { level: 3 }).allTextContents());
const related = page.getByRole('complementary', { name: 'Customers also bought' });
console.log('related links:', await related.getByRole('link').allTextContents());
console.log('banners:', await page.getByRole('banner').count());
await browser.close();
})();
review headings: [ 'Worth reading twice' ]
related links: [ 'Refactoring' ]
banners: 1
Div soup sayfasında bu sorguların hiçbiri çalışmaz; testi .sidebar a gibi bir CSS selector'üne bağlamak zorunda kalırsın ve tasarımcı class adını değiştirdiği gün test kırılır. Role tabanlı locator, ekran okuyucunun gördüğü yapıyı test ettiği için aynı zamanda küçük bir accessibility kontrolü.
Üçüncü faydalanıcı makineler. Tarayıcıların okuma görünümleri, arama motorlarının crawler'ları ve AI agent'ları sayfanın asıl içeriğini navigasyon, reklam ve footer'dan ayırmak zorunda. main, article ve doğru heading'ler bu ayrımı açıkça söyler. GEO (Generative Engine Optimization) serisinin "AI Agent'lar İçin Site: Semantic HTML, Accessibility Tree ve Lighthouse Agentic Browsing" yazısında, browser agent'larının sayfayı büyük ölçüde accessibility tree üzerinden anladığını ve ekran okuyucunun ihtiyaç duyduğu yapının agent'ın ihtiyaç duyduğu yapıyla neredeyse aynı olduğunu görmüştük. Bu yazıdaki iki ağaç arasındaki fark, bir agent'ın "Customers also bought" kutusunu bulup bulamaması demek.
Üretimde Dikkat
Landmark yapısını sayfa sayfa değil, layout düzeyinde kur. Bookstore'da header, nav, main ve footer tek bir yerde, ASP.NET Core'da _Layout.cshtml'de, Angular'da shell component'inde yaşamalı; sayfalar yalnız main'in içini doldurur. Bu, "iki main" ve "iki banner" hatalarını kaynağında keser.
Ölçmek için iki hızlı yöntem var. DevTools'ta Elements panelindeki Accessibility sekmesi, seçtiğin element'in role'ünü ve adını gösterir; Browser DevTools serisinde bu paneli ayrıntılı gördük. Otomatik kontrol için yukarıdaki axtree.js gibi bir script'i ya da Playwright'ın ariaSnapshot()'ını kritik sayfalarda çalıştırıp çıktıyı snapshot test olarak saklayabilirsin; biri main'i silerse ya da bir heading seviyesini değiştirirse diff'te görürsün.
Son olarak, landmark sayısını makul tut. Her kutuyu section aria-labelledby yapıp yirmi region üreten bir sayfa, landmark listesini sıfır landmark kadar kullanışsız hâle getirir. Landmark, kullanıcının atlamak isteyeceği büyük bölgeler içindir: genellikle bir banner, bir veya iki navigation, bir search, bir main, birkaç complementary ve bir contentinfo yeter.
Sık Düşülen Tuzaklar
Birincisi, section'ı süslü bir div gibi kullanmak. Adsız section accessibility tree'de div'den farksız; stil kutusu için div yaz.
İkincisi, her bölümde h1 kullanıp outline algoritmasının seviyeleri düzelteceğini sanmak. Algoritma hiç uygulanmadı ve 2022'de spec'ten kaldırıldı; seviyeleri elle kur.
Üçüncüsü, birden fazla nav'ı adsız bırakmak. Ekran okuyucu listesinde iki tane "navigation" görünür ve kullanıcı hangisinin ana menü olduğunu tahmin etmek zorunda kalır. aria-label ile "Main", "Footer", "Breadcrumb" gibi adlar ver.
Dördüncüsü, search'ü arama sonuçları için kullanmak ya da içine form koymayı unutmak. search yalnız bir landmark; sorgu parametresini sunucuya hâlâ form gönderiyor.
Beşincisi, semantic element'leri eklerken eski role attribute'larını yerinde bırakmak: <nav role="navigation"> ya da <main role="main">. Zararsız ama gereksiz; checker bunlar için "The “navigation” role is unnecessary for element “nav”." gibi uyarılar veriyor, çünkü native element zaten aynı role'ü taşıyor.
Küçük Bir Deneme
★☆☆ Yazıdaki soup.html ve semantic.html'i kaydet, ikisini Chrome'da aç ve DevTools'ta Elements panelinin Accessibility sekmesinde "Enable full-page accessibility tree" seçeneğini aç. İki sayfanın ağacını karşılaştır ve figürdeki eşlemeyi kendi gözünle doğrula.
★★☆ semantic.html'e footer'da "Privacy" ve "Terms" linklerini içeren ikinci bir nav ekle. Önce adsız bırak, axtree.js çıktısına bak; sonra aria-label="Footer" ver ve farkı not et. Aynı sırada section'lardan birinin aria-labelledby'ını sil ve region'ın kaybolduğunu gör.
★★★ Kendi projenden bir sayfanın render edilmiş HTML'ini al ve axtree.js'i ona çalıştır. Landmark'ları ve heading seviyelerini bir kâğıda çiz: tek bir main var mı, h1 sayfanın konusunu mu söylüyor, atlanan seviye var mı? Bulduğun sorunları layout ve component düzeyinde nasıl düzelteceğini planla.
Landmark'lar sayfanın büyük bloklarını adlandırdı. Bir sonraki yazıda bu blokların içine iniyoruz: metin semantiği. strong ile b arasındaki fark, time, abbr, dl ile kitap künyesi, ve en çok yanlış yapılan karar: link mi, button mı?