
Eğitim Uygulaması
Tıp ve sağlık bilimleri öğrencileri için anatomi atlası. Veritabanı yok: içerik depoda duruyor, derleme anında şemaya göre doğrulanıyor ve sayfaların tamamı statik üretiliyor. Arama, quiz ve çevrimdışı kullanım tarayıcıda çalışıyor.
Anatomi içeriği iki farklı türden oluşuyor: bir yanda tamamen yapısal veri — kasın başlangıcı, yapışma yeri, siniri, hareketi — öbür yanda serbest anlatı, klinik not, ayrım tanısı. İlkini bir metin dosyasında tutmak veriyi aranamaz ve tutarsız hâle getiriyor; ikincisini bir veritabanı tablosuna sıkıştırmak ise yazmayı imkânsızlaştırıyor. Asıl mesele ise doğruluk: eğitim içeriğinde bir alanın boş kalması ya da bir referansın var olmayan bir yapıya işaret etmesi, çalışan öğrenci için sessiz bir hata. Sayfa açılıyor, tablo görünüyor, yalnız bir satır eksik. Bu tür bir hatanın yayına çıkmadan yakalanması gerekiyordu.
Tek bir kullanıcı tipi ama iki farklı kullanım anı var. Çalışan öğrenci: uzun oturumlarda okuyor, bir yapıdan diğerine sürekli atlıyor — kastan sinire, sinirden bölgeye — ve arama kutusunu klavyeden açmak istiyor. Tekrar eden öğrenci: sınav öncesi, ders arasında, telefondan, çoğu zaman kötü bir bağlantıyla. İkinci kullanım biçimi çevrimdışı çalışmayı ve hızlı açılışı zorunlu kılıyor.
Tek geliştirici. İçerik şeması ve doğrulama hattı, veri modeli, statik üretim yapısı, arama, quiz akışı, çevrimdışı katman ve testler.
Sistemin merkezinde bir karar var: veritabanı yok. Yapısal veriler depoda JSON olarak, anlatı içeriği MDX olarak duruyor; detay sayfasında ikisi bir yükleyici üzerinden birleşiyor. İlişkiler kimlik yerine slug ile kuruluyor, yani bir kasın siniri "sinir-42" değil, o sinirin okunabilir adı — bu hem adresleri hem de içerik dosyalarını insan tarafından düzenlenebilir tutuyor. Tüm detay sayfaları `generateStaticParams` ile derleme anında üretiliyor; çalışma anında sunucuya giden bir okuma yolu yok. Varsayılan render biçimi Server Component; `"use client"` yalnız gerçek etkileşim olan yerde. Tarayıcı tarafında üç şey çalışıyor: Fuse.js ile bulanık arama — index istemcide kuruluyor, masaüstündeki ⌘K modalı ve mobildeki tam ekran katman aynı gövdeyi paylaşıyor — Zustand'da tutulan kişisel durum ve service worker önbelleği. Zustand'da içerik yok, yalnız kullanıcıya ait olan şey var: yer imi, ilerleme, quiz durumu.
İçerik günde yüz kez değişmiyor; dönemde birkaç kez, üstelik gözden geçirilerek değişiyor. Böyle bir içerik için veritabanı, çalışma anında sorgu, bağlantı yönetimi ve ayrı bir yönetim arayüzü maliyeti getiriyor — karşılığında verdiği tazelik ise gerekmiyor. Bunun yerine içerik sürüm kontrolünde: her değişiklik gözden geçirilebilir bir fark olarak duruyor ve geri alınabiliyor.
Ödünleşimİçeriği değiştirmek için depoya erişim ve yeni bir dağıtım gerekiyor; alan uzmanı bir kullanıcı tek başına düzeltme yapamıyor. Bir yazım hatası bile bir build turu bekliyor.
Doğrulama script'i bütün veri dosyalarını Zod şemalarıyla ayrıştırıyor ve tek bir geçersiz kayıtta sıfırdan farklı çıkıyor. TypeScript tipleri de aynı şemadan türetiliyor, yani şema tek kaynak: tip ile çalışma anındaki doğrulama ayrışamıyor. Eğitim içeriğinde bu kapı, "sayfa açılıyor ama bilgi eksik" hatasını yayına çıkmadan yakalayan tek mekanizma.
ÖdünleşimŞema katı olduğu için henüz tamamlanmamış bir içerik parçası depoya giremiyor; taslak hâlinde çalışmak isteyen biri ya alanı boş bırakma iznini şemaya eklemek ya da içeriği depo dışında tutmak zorunda. Şemanın gevşetilmesi ise kapının değerini doğrudan düşürüyor.
Aranan veri kümesi küçük ve sabit; derleme anında hazır olan bir index'i istemciye göndermek, her tuş vuruşunda sunucuya gitmekten hem hızlı hem de basit. Bulanık eşleşme, Latince terim ile Türkçe karşılığın aynı sorguda bulunabilmesi için gerekiyordu. Masaüstü modalı ile mobil katman aynı gövdeyi ve aynı index'i paylaşıyor.
ÖdünleşimIndex istemciye indiriliyor; içerik büyüdükçe bu maliyet büyüyecek ve bir noktada sunucu tarafı aramaya geçmek gerekecek. Ayrıca hangi terimin arandığına dair hiçbir veri toplanmıyor — bu gizlilik açısından iyi, içerik önceliklendirmesi açısından bir körlük.
Yer imi, ilerleme ve quiz durumu kullanıcıya ait ve sunucuda saklanmasının bir karşılığı yok — hesap yok, senkronizasyon yok. Buna karşılık içeriğin istemci store'una kopyalanması yasak: içerik sunucudan gelen tek kaynaktır, ikinci bir kopyası tutulursa iki kaynak zamanla ayrışıyor.
ÖdünleşimKullanıcı cihaz değiştirdiğinde ilerlemesi taşınmıyor; tarayıcı verisi temizlendiğinde de kayboluyor. Hesap açmadan çalışmanın bedeli bu ve kabul edildi.
Next.js App Router burada bir sunucu çatısı olarak değil, bir statik site üreticisi olarak kullanılıyor: `generateStaticParams` ile yüzlerce detay sayfası derleme anında çıkıyor ve Server Component varsayılan olduğu için istemciye giden JavaScript en aza iniyor. Zod'un iki işi var — hem doğrulama kapısı hem tip kaynağı; ikisinin tek yerden gelmesi, şemayı değiştirdiğinde tipin de değişmesini garanti ediyor. MDX, klinik anlatının yazı gibi yazılmasına izin verirken bileşen gömmeyi de açık bırakıyor. Fuse.js küçük ve istemcide çalışabilecek kadar hafif. Serwist, çevrimdışı katmanı service worker yazmadan kuruyor. Görsel tarafta etkileşimli SVG, yakınlaştırma ve kaydırmayı raster bir görselin bulanıklığına düşmeden veriyor.
Uygulamanın yazma yolu yok: form, oturum, hesap ve kullanıcı veritabanı bulunmuyor. Dolayısıyla saklanan kişisel veri de yok — ilerleme ve yer imi kullanıcının cihazından çıkmıyor. İçerik üretimi sırasında kullanılan model çağrıları yalnız script'lerde; çalışma anında hiçbir dış servise istek gitmiyor. Bu, saldırı yüzeyini içeriğin doğruluğuna indiriyor — ve o da derleme kapısıyla korunuyor.
Bu proje için saklanmış, tarih damgalı bir Lighthouse çıktısı bulunmadığı için buraya sayfa hızı skoru yazılmadı. Yapısal tarafta ölçü şu: okuma yolunda sunucu sorgusu yok, sayfalar derleme çıktısı olarak sunuluyor. Arama index'i ve quiz durumu istemcide olduğu için etkileşim gecikmesi ağ turuna bağlı değil. Service worker son görülen sayfaları önbellekte tutuyor; bağlantı koptuğunda uygulama boş ekran yerine kendi çevrimdışı sayfasını gösteriyor.
Doğrulama kapısını baştan koydum ve bu projede en çok işe yarayan karar oydu; aynı deseni sonradan Rahmina'da içerik onay kapısı olarak tekrarladım — orada kural "şemaya uygun mu" değil, "gözden geçirildi mi" oldu. Buradaki eksik tam olarak o: şema bir alanın dolu olduğunu doğruluyor ama içeriğin doğru olduğunu doğrulamıyor. Bugün her içerik bloğuna bir gözden geçirme durumu ekler ve yayın kapısını "onaysız içerik varsa build düşer" hâline getirirdim. İkinci nokta arama: index'in istemciye inmesi bugünkü hacimde doğru karar, ama bu kararın ne zaman bozulacağını ölçen bir şey yok. Bugün index boyutunu bir eşikle sınırlar, eşik aşıldığında derlemeyi uyarır hâle getirirdim — kararın kendisi kadar, kararın ne zaman geçersizleşeceği de kod içinde durmalı.
Ne yapmak istediğinizi anlatın; ne kadar süreceğini ve nereden başlanacağını baştan söylerim.