Mert Özen Her Satırda Daha İleri
Frontend UI/UX Tasarım İlkeleri — #08

Navigation ve Information Architecture: Menü, Breadcrumb, Arama, Filtre ve Pagination Kararları

Mert Özen 14 Kas 2026 12 dk 8 görüntülenme
Navigation ve Information Architecture: Menü, Breadcrumb, Arama, Filtre ve Pagination Kararları

İyi bir katalog navigation'ı listenin state'ini URL'de tutar, kategoriyi hiyerarşi ve formatı facet olarak ayırır, aktif filtreleri görünür kılar ve aranıp karşılaştırılan listelerde infinite scroll yerine pagination kullanır. Bookstore kataloğunda yenilemede kaybolan filtreleri, kullanıcıyı katalogdan çıkaran Back tuşunu, 70 kitap yüklenmeden görünmeyen footer'ı ve sıfır link'li listeyi ölçüp pushState/replaceState, facet sayıları, chip'ler, breadcrumb ve sonuç yok kurtarmasıyla düzeltiyoruz.

Sürüm notu: Bu yazıdaki örnek ekranlar Playwright 1.65 ile Chromium 158'de render edilip ölçülmüştür; script'ler Node.js 24 ile çalıştırılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için bookstore-web'in katalog sayfasında 30 kitabın 70 baskısı var. Solda kategori ve format checkbox'ları ile bir sıralama select'i, sağda kartlar duruyor; liste aşağı inildikçe 12'şer kitap ekleyerek uzuyor. Filtrelerin tutulduğu yer ve yükleme mantığı şu:

const state = { categories: new Set(), formats: new Set(), sort: 'relevance', q: '' };

// filtre state'i yalnız bu değişkende: URL hiç değişmiyor
function render() {
  shown = 0;
  grid.innerHTML = '';
  more();
}

// infinite scroll: listenin sonu görünür olunca 12 kitap daha
new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting && shown < results().length) more();
}, { rootMargin: '200px' }).observe(document.querySelector('#sentinel'));

Kartlar onclick ile kitap sayfasına giden div'ler. Ada "Classics" ve "Paperback"i seçip sayfayı yenilerse ne görür? Filtreyi geri almak için tarayıcının Back tuşuna basarsa nereye gider? Listede 40. kitabı açıp geri döndüğünde o kitap nerede olur? Sayfanın en altındaki "Returns policy" linkine ulaşmak için kaç kitabın yüklenmesini beklemesi gerekir? Gothic kategorisinde, Hardcover formatında "Austen" arayan biri ne görür?

Bu yazı tek bir ekrandan sitenin yapısına çıkıyor: içerik nasıl gruplanır, kullanıcı nerede olduğunu nereden anlar, bir listeyi daraltmak, sıralamak ve sayfalara bölmek hangi kararları gerektirir.

Önce ölçelim

Script her iki sayfada aynı yolu izliyor: önce başka bir sayfadan kataloğa geliyor, iki filtre seçiyor, URL'yi ve gösterilen sonuç sayısını yazıyor, sayfayı yeniliyor, Back'e basıyor; sonra footer görünene kadar aşağı kaydırıp o anda sayfada kaç kart olduğunu sayıyor, listede bir crawler'ın (arama motoru botu) izleyebileceği link'leri sayıyor, 40. kitabı açıp geri dönüyor ve sonuçsuz bir filtre kombinasyonunda ekranın ne önerdiğine bakıyor:

node scripts/08-navigation.mjs
--- before
  filtered to Classics + Paperback: url 08-catalog/before.html, (no count shown)
  after reload: active filters (none)
  Back: url 05-spacing/after.html, active filters (left the catalog)
  footer: in view once 70 of 70 cards are on the page
  links in the list a crawler can follow: 0
  open the 40th book, Back: "Emma" not on the page, url 08-catalog/before.html
  "Austen" in Gothic + Hardcover: "No results", 0 way(s) forward
--- after
  filtered to Classics + Paperback: url 08-catalog/after.html?category=classics&format=paperback, 13 books, showing 1–12
  after reload: active filters paperback, Classics
  Back: url 08-catalog/after.html?category=classics, active filters Classics
  footer: in view once 12 of 70 cards are on the page
  links in the list a crawler can follow: 17
  open the 40th book, Back: "Emma" in view, url 08-catalog/after.html?page=4
  "Austen" in Gothic + Hardcover: "No books match these filters", 2 way(s) forward: Search all categories (3 books) | Clear all filters (70 books)

Before'da filtreler URL'ye yansımadığı için yenilemek hepsini siliyor ve Back filtreyi geri almak yerine kullanıcıyı katalogdan çıkarıyor. Footer ancak bütün 70 kitap 12'şer 12'şer yüklendikten sonra görünüyor; her kaydırma yeni kitap getirip footer'ı tekrar aşağı itiyor. Listede tek bir gerçek link yok. 40. kitaptan geri dönünce liste baştan, 12 kitapla yükleniyor ve Emma sayfada değil. Sonuçsuz filtre kombinasyonu "No results" deyip bırakıyor.

Bilgi Mimarisi: Önce İçeriği Grupla

Information architecture (IA; içeriğin nasıl gruplanıp adlandırıldığı ve birbirine nasıl bağlandığı), menüyü çizmeden önce verilen karar. Bookstore için iki farklı türde gruplama var ve bunları karıştırmak before'un ilk hatası. Kategori bir hiyerarşi: bir kitap Classics, Science fiction, Fantasy ya da Gothic'ten birinde duruyor, kullanıcı bu kategoriler arasında "gezinir". Format ise bir facet (listeyi daraltan bağımsız bir özellik): Paperback, Hardcover ve E-book aynı anda seçilebilir, kullanıcı bunlarla listeyi "filtreler". Before ikisini de checkbox yapmış; after'da kategori üst menüde görünür link'ler, format solda checkbox'lar.

Kategorilerin adlarını ve gruplarını ekip toplantısında değil kullanıcıyla belirlemek daha sağlıklı. Card sorting bunun en bilinen yöntemi: katılımcılar her biri bir sayfayı ya da kavramı temsil eden kartları kendilerine mantıklı gelen gruplara ayırıyor. Nielsen Norman Group'un (NN/g; Jakob Nielsen ve Don Norman'ın 1998'de kurduğu kullanılabilirlik araştırma şirketi) tanımına göre açık card sorting'de grupları ve adlarını katılımcı kendisi koyuyor, kapalı card sorting'de önceden belirlenmiş gruplara yerleştiriyor; nitel bir çalışma için en az 15 katılımcı öneriliyor. Kullanıcı görüşmesi ve discovery çalışmasının kendisi Product Management'a Giriş serisinin discovery yazısında; burada sonucun ekrana nasıl yansıdığına bakıyoruz.

URL de Bir Arayüz

Kullanıcı URL'yi okumasa bile URL'yi kullanıyor: yenileme, Back ve Forward, yer imi, link paylaşma ve yeni sekmede açma hep URL üzerinden çalışıyor. Bir listenin ne gösterdiği URL'de yoksa bu işlemlerin hepsi bozuluyor. After'da listenin state'i tek bir yerde, URL'de duruyor; sayfa her render'da onu okuyor:

// listenin ne gösterdiğinin tek kaynağı URL
function readState() {
  const p = new URLSearchParams(location.search);
  return {
    category: p.get('category') ?? '',
    formats: p.getAll('format'),
    sort: p.get('sort') ?? 'relevance',
    q: p.get('q') ?? '',
    page: Math.max(1, Number(p.get('page')) || 1),
  };
}

URL'yi değiştirmenin iki yolu var ve hangisinin seçileceği bir UX kararı. history.pushState geçmişe yeni bir kayıt ekliyor, Back bu adımı geri alıyor. history.replaceState mevcut kaydı değiştiriyor, Back bu adımı atlıyor. MDN'e (Mozilla'nın web dokümantasyonu) göre iki çağrı da popstate tetiklemiyor; event yalnız kullanıcı geçmişte ileri ya da geri gittiğinde geliyor, bu yüzden render'ı o event'e de bağlamak gerekiyor:

// filtre değişikliği, kullanıcının Back ile geri almak isteyebileceği bir adım: pushState
$('#filters').addEventListener('change', () => {
  const s = readState();
  const formats = [...document.querySelectorAll('[name=format]:checked')].map((i) => i.value);
  history.pushState(null, '', toUrl({ ...s, formats, page: 1 }));
  render();
});
// aynı listenin farklı sırası değil: replaceState
$('#sort').addEventListener('change', (event) => {
  history.replaceState(null, '', toUrl({ ...readState(), sort: event.target.value, page: 1 }));
  render();
});
window.addEventListener('popstate', render);

Ölçümdeki "Back: url …?category=classics, active filters Classics" satırı bu kararın sonucu: Back son filtreyi kaldırıyor, kullanıcıyı katalogdan çıkarmıyor. Sıralamayı replaceState ile tutmamızın sebebi de aynı mantık: sırayı beş kez değiştiren biri katalogdan çıkmak için beş kez Back'e basmak zorunda kalmamalı. Filtre değişince sayfa numarasını 1'e çekmek de önemli; üçüncü sayfadayken liste 13 kitaba düşerse kullanıcı boş bir sayfaya bakar. Angular'da aynı fikir router'ın query parametreleriyle kuruluyor.

Facet Filtreler, Sayılar ve Aktif Filtre Chip'leri

İyi bir facet filtre iki soruyu önceden cevaplıyor: bu seçeneği işaretlersem kaç sonuç kalır ve şu an hangi filtreler açık? After her format seçeneğinin yanına, diğer filtreler korunarak kaç kitap kalacağını yazıyor: Classics içinde "Paperback (13)", "Hardcover (11)", "E-book (7)". Kullanıcı sıfır sonuçla biten bir seçeneğe tıklamadan önce bunu görüyor. Liste başlığının altındaki "13 books, showing 1–12" satırı da listenin ne kadar büyük olduğunu ve kullanıcının nerede olduğunu söylüyor; role="status" sayesinde filtre değişince ekran okuyucu yeni sayıyı duyuruyor.

Açık filtreler sol panelde, ekranın kenarında kalıyor ve uzun bir panelde kaydırınca görünmüyor. Bu yüzden after aktif filtreleri listenin hemen üstünde chip olarak tekrar gösteriyor. Her chip bir link: href'i o filtre çıkarılmış URL, yani "Format: Paperback ×" chip'ine tıklamak, ya da onu yeni sekmede açmak, aynı listeyi Paperback olmadan veriyor. "Clear all" hepsini birden kaldırıyor.

08-catalog-filters-tr.png

Breadcrumb, sayfanın sitenin hiyerarşisindeki yerini gösteren link dizisi. NN/g'nin 2018 tarihli breadcrumb rehberi birkaç kuralı net koyuyor: breadcrumb kullanıcının izlediği yolu değil, sayfanın hiyerarşideki konumunu gösterir; ana navigation'ın yerine geçmez, onu tamamlar; son eleman bulunulan sayfadır ve link değildir; düz ya da tek adımlı sitelerde gereksizdir. Mobilde birden fazla satıra taşmasını ve dokunması zor küçük link'leri önlemek için yalnız son bir iki seviyeyi göstermeyi öneriyor.

// breadcrumb: kullanıcının izlediği yol değil, listenin hiyerarşideki yeri
const crumbs = [`<li><a href="after.html">Home</a></li>`];
if (category && s.q) crumbs.push(`<li><a href="${toUrl({ ...s, q: '', formats: [], page: 1 })}">${category.name}</a></li>`);
crumbs.push(`<li aria-current="page">${title}</li>`);
$('#crumbs').innerHTML = crumbs.join('');

Liste nav aria-label="Breadcrumb" içinde bir ol; son eleman aria-current="page" taşıyor ve link değil. Ayırıcı "›" CSS'te ::before ile geliyor, böylece ekran okuyucu her adımda ayırıcı karakteri okumuyor. Üst menüdeki aktif kategori de aynı attribute'la işaretli: Classics sayfasındayken "Classics" link'inin altında bir çizgi var. Breadcrumb'ın arama motorlarına BreadcrumbList olarak bildirilmesi Structured Data ve Schema.org serisinin konusu.

Arama: Öneri ve "Sonuç Yok" Kurtarma

Arama kutusu kullanıcının kendi kelimeleriyle yaptığı bir navigation. Bir önceki yazıdaki yazım önerisine ek olarak after iki şey yapıyor. Arama kutusu bir datalist'e bağlı; kullanıcı yazdıkça tarayıcı eşleşen kitap ve yazar adlarını öneriyor. Native datalist görünüşü özelleştirilemediği ve tarayıcıdan tarayıcıya farklı davrandığı için büyük bir katalogda yerini sunucudan gelen önerilere ve erişilebilir bir combobox'a bırakır, ama küçük bir listede ek kod gerektirmeden çalışıyor.

İkincisi, filtrelerle birleşen bir aramada sonuç yoksa after hangi filtrenin buna sebep olduğunu hesaplıyor ve her birini kaldırmanın kaç sonuç getireceğini söylüyor:

// filtreli "sonuç yok": her filtrenin neye mal olduğunu göster
function noResults(s) {
  const options = [
    ...(s.category ? [[`Search all categories`, { ...s, category: '' }]] : []),
    ...s.formats.map((f) => [`Remove "${formatNames[f]}"`, { ...s, formats: s.formats.filter((x) => x !== f) }]),
  ].map(([label, next]) => [label, next, filter(next).length]).filter(([, , n]) => n > 0);

Ölçümde "Austen" Gothic + Hardcover'da sonuç vermiyor; after "Search all categories (3 books)" ve "Clear all filters (70 books)" öneriyor. Hardcover filtresini kaldırmak Gothic'te yine sıfır sonuç verdiği için o seçenek hiç gösterilmiyor: öneri listesindeki her link en az bir kitaba çıkıyor. Hangi aramaların sonuçsuz kaldığını kaydetmek, kataloğa eksik kitabı ya da eşanlamlı kelimeyi eklemenin en ucuz yolu; bunun ölçümü "Analytics ve Ölçüm" serisinde.

Pagination, "Daha Fazla Yükle" ve Infinite Scroll

Üç yaklaşımın her birinin bir bedeli var:

KonuPaginationDaha fazla yükleInfinite scroll
Footer'a erişimHer sayfadaButonun altındaListe bitene kadar zor
Kaldığı yere dönmekURL'deki sayfa numarasıylaState saklanmazsa kaybolurState saklanmazsa kaybolur
Arama motoruHer sayfa bir a hrefEk link'ler gerekirEk link'ler gerekir
Listenin boyunu bilmek"Page 4 of 6"KısmenYok
En uygun olduğu yerAranıp karşılaştırılan listeGöz gezdirilen, orta boy listeAmaçsız göz atılan akış

NN/g'nin 2022 tarihli infinite scrolling yazısı infinite scroll'u haber ya da sosyal medya akışı gibi belirli bir hedef olmadan göz atılan içerikte uygun buluyor; belirli bir şeyi aranan, karşılaştırılan listelerde, footer'daki bilgi önemliyken ve yavaş bağlantılı ya da erişilebilirlik ihtiyacı olan kullanıcılarda sorunlu buluyor. Yazının işaret ettiği bir sorun da dönüş: bir ürünü açıp geri gelen kullanıcıyı listenin başına atmak. Ölçümde tam olarak bu oldu; Chromium sayfayı geri dönüşte yeniden yükledi, before 12 kitapla başladı ve Emma sayfada yoktu. After'da Emma 4. sayfada, URL ?page=4 ve geri dönüşte sayfa aynı listeyi kurduğu için tarayıcının kaydırma konumunu geri yüklemesi yetti.

Google'ın e-ticaret pagination rehberi arama motoru tarafını tamamlıyor: Googlebot butonlara tıklamıyor ve kullanıcı eylemiyle tetiklenen JavaScript'i çalıştırmıyor, bu yüzden "daha fazla yükle" ve infinite scroll ile gelen ürünler bulunamayabiliyor. Rehber her sayfaya ?page=n gibi kendi URL'sini vermeyi, sayfaları sıradaki sayfaya standart a href link'leriyle bağlamayı, sayfa numarası için URL fragment'ı (# sonrası) kullanmamayı ve her sayfanın canonical'ını ilk sayfaya değil kendisine vermeyi öneriyor; rel="next" ve rel="prev"'i artık kullanmadığını da belirtiyor. Ölçümdeki "links … a crawler can follow: 0" ile "17" farkı buradan geliyor: before'un kartları onclick'li div'ler, after'ınkiler gerçek link. Bookstore kataloğu için seçim pagination; "daha fazla yükle" de makul bir seçenek olurdu, yeter ki her yüklemeden sonra URL'deki sayfa numarası güncellensin.

Mobil Navigation: Bottom Bar, Hamburger ve Jakob Yasası

Dar ekranda üst menüdeki dört kategori ve arama kutusu sığmıyor; iki yaygın çözüm var. Hamburger menü her şeyi bir butonun arkasına saklıyor. NN/g'nin 2016'daki 179 katılımcılı çalışmasında gizli navigation masaüstünde katılımcıların yalnız yüzde 27'sinde kullanılmış, görünür navigation'da bu oran yüzde 48; içeriğin bulunabilirliği gizli navigation'da yüzde 20'den fazla düşmüş. Mobilde gizli navigation yüzde 57, görünür ve gizli link'leri birleştiren navigation yüzde 86 oranında kullanılmış. Önerileri de buna göre: masaüstünde navigation'ı gizleme, mobilde en fazla dört ana link'i görünür bırak.

Bottom bar (alt navigation çubuğu) ana bölümleri başparmağın yetiştiği yerde görünür tutuyor. Material 3'ün navigation bar rehberi bunu üç ile beş ana hedef için öneriyor ve beşten fazla öğede kullanılmamasını söylüyor; Apple'ın Human Interface Guidelines'ı da tab bar'ın eylem için değil navigation için kullanılmasını istiyor. Bookstore'un mobil uygulaması için aday dört bölüm belli: Home, Categories, Search, Cart. Başparmak erişimi ve dokunma hedefleri bir sonraki yazının konusu.

Hangisini seçersen seç, Jakob yasası bir sınır çiziyor: Jakob Nielsen'in adıyla anılan bu kurala göre kullanıcılar zamanlarının çoğunu başka sitelerde geçirdiği için senin sitenin de onlar gibi çalışmasını bekliyor. Sepet ikonunun sağ üstte, logonun ana sayfaya gitmesi, aktif filtrelerin chip olarak görünmesi bu beklentiler. Before'un logosu bir link değil, tıklanınca sayfayı yeniliyor; bu küçük sapma bile kullanıcıya bir şey öğretmek zorunda kalıyor. Alışılmış bir pattern'den sapmak ancak ölçülmüş ve belirgin bir kazanç varsa buna değer.

Üretimde Dikkat

Filtre URL'leri arama motoru için bir risk. Format, sıralama ve sayfa kombinasyonları aynı listenin binlerce varyasyonunu üretiyor. Google'ın aynı rehberi, filtreli ve farklı sıralanmış URL'leri index dışında tutmak için bu sayfalara noindex koymayı ya da bu URL pattern'lerini robots.txt ile taramaya kapatmayı öneriyor. Hangi kombinasyonların index'e girmesi gerektiği bir ürün kararı; "Hardcover klasikler" gerçekten aranan bir sayfaysa kendi temiz URL'sini hak edebilir. Canonical ve noindex'in mekaniği Teknik SEO (arama motoru optimizasyonu) serisinde.

URL parametrelerinin sırası ve biçimi tutarlı olmalı. toUrl parametreleri her zaman aynı sırayla yazıyor ve varsayılan değerleri (sıralama "relevance", sayfa 1) URL'ye koymuyor; böylece aynı liste her zaman aynı URL'ye sahip oluyor, cache ve analytics tarafında da tek kayıt olarak görünüyor.

Facet sayıları büyük katalogda pahalı. Burada 70 kitap tarayıcıda sayılıyor; gerçek Bookstore'da bu sayılar arama altyapısının aggregation'larından gelir ve sunucu tarafında cache'lenir.

Sık Düşülen Tuzaklar

Birincisi, filtreyi yalnız JavaScript state'inde tutmak. Yenileme, Back, paylaşma ve yeni sekme hepsi bozuluyor; listenin state'i URL'de olmalı.

İkincisi, her değişikliği pushState ile geçmişe yazmak. Kullanıcının geri almak isteyeceği adımlar (filtre) geçmişe girer, ayar niteliğindeki değişiklikler (sıralama) girmez.

Üçüncüsü, kartları div ve onclick ile link'e çevirmek. Gerçek bir a href hem yeni sekmede açılıyor hem arama motoru tarafından izleniyor hem de klavyeyle ulaşılıyor.

Dördüncüsü, aranan ve karşılaştırılan bir katalogda infinite scroll. Footer kayboluyor, dönüşte yer kayboluyor, liste arama motoruna görünmüyor.

Beşincisi, kategori ile facet'i karıştırmak. Hiyerarşi navigation olur, bağımsız özellikler filtre olur.

Küçük Bir Deneme

★☆☆ After'a "Language" adında ikinci bir facet ekle (English, Turkish) ve facet sayılarının diğer filtreleri koruyarak hesaplandığını doğrula: Classics + Paperback seçiliyken Language seçeneklerinin yanındaki sayıları önce tahmin et, sonra ekrandan oku.

★★☆ Pagination yerine "Load more" butonu dene: her tıklamada 12 kitap eklensin ve URL replaceState ile ?page=n'e güncellensin. Script'teki 40. kitap testini bu sürüm için çalıştır ve Emma'nın geri dönüşte görünüp görünmediğine bak.

★★★ Mobil genişlikte (390 px) kategori menüsünü bir bottom bar'a taşı ve dört ana bölümü görünür tut. Script'e, bottom bar'ın footer'daki link'leri örtmediğini ve klavye focus'unun bottom bar'ın altında kalmadığını kontrol eden bir adım ekle.

Bir sonraki yazıda bu kataloğu ve sepeti telefona taşıyoruz: içeriği mobilde önceliklendirmek, breakpoint'i cihaza değil içeriğin kırıldığı yere koymak, WCAG (Web Content Accessibility Guidelines; web erişilebilirlik standardı) 2.2'nin 24 piksellik hedef boyutu ile platform rehberlerinin daha büyük önerileri arasındaki fark, başparmak erişim alanı ve dark mode'u bir tema olarak tasarlamak.