Bir ürün listesini filtrelediğinizde tarayıcının ekranı bir an beyaza dönüp sayfayı baştan çizdiği anı herkes bilir. Kaydırdığınız yer kaybolur, odaklandığınız arama kutusu boşalır, geri tuşu da beklenmedik bir yere götürür. Yıllarca bu sorunun cevabı, bütün arayüzü tarayıcıda çalışan bir JavaScript uygulamasına taşımak oldu. Bedeli de bilinen bir liste: büyük paketler, ayrı bir durum yönetimi, sunucu ile istemci arasında çoğaltılmış veri ve her yeni özellikte yeniden yazılan bir API katmanı.
HTMX bu zincirin bir halkasını başka bir yerden çözüyor. Tarayıcının zaten bildiği şeyleri, yani bağlantıları, formları ve HTML parçalarının sayfanın içine yerleştirilmesini öne çıkarıyor. Sunucu tam bir sayfa yerine yalnızca değişmesi gereken parçayı döndürüyor; HTMX bu parçayı doğru yere yerleştiriyor. Ortaya çıkan şey sayfa yenilenmeden çalışan, durumu tek bir yerde, sunucuda tutan bir arayüz.
Bu metinde HTMX'in neyi çözdüğünü, neyi bilerek çözmediğini ve küçük bir arama kutusunu uçtan uca nasıl kurduğumuzu anlatacağım. Örnekler Laravel ile yazıldı, ama mantık herhangi bir sunucu çatısında aynı.
Hiper medyanın eski ama güçlü modeli
HTML'in ilk tasarımında her bağlantı bir durum geçişiydi. Tıkladığınızda tarayıcı yeni bir kaynak istiyor, sunucu bu kaynağın temsilini döndürüyor ve tarayıcı onu gösteriyordu. Hiper medya denen bu model, web'i bugüne taşıyan şeydi. Tek sayfa uygulamaları ortaya çıktığında ise bu akış büyük ölçüde terk edildi: sunucu veri gönderiyor, istemci o veriden ekranı kendisi çiziyordu.
Bu geçiş bir süre sonra bedelini gösterdi. Aynı bilgi hem sunucuda hem istemcide bulunuyordu, ikisi ayrıştığında hata ayıklamak zorlaşıyordu. Her ekran için iki kez kod yazıyordunuz: bir kez şablon olarak, bir kez de istemci tarafında. HTMX'in önerdiği model bu ikiliği kaldırmak için sade bir kural koyuyor: ekranın kendisi sunucuda üretilir, istemci yalnızca nereye ve ne zaman yerleşeceğine karar verir.
Bir arayüzü hızlı yapan şey, istemcide ne kadar kod çalıştırdığınız değil; kullanıcının beklediği süreyi ne kadar kısalttığınızdır.
Bu bakış açısı yalnızca performansla ilgili değil. Kodun nerede yaşadığını da belirliyor. Bir kural değiştiğinde yalnızca sunucudaki şablonu düzenliyorsanız, değişikliğin iki yerde tutarlı kalıp kalmadığını kontrol etmek zorunda kalmıyorsunuz.
Üç öznitelik, bir akış
HTMX'in temel sözdizimi HTML öznitelikleriyle yazılır. Bir öğe bir olayı dinler, sunucuya istek gönderir, dönen parçayı bir hedefe yerleştirir. Bu dört adımı açıklayan dört öznitelik çoğu işi görür.
| Öznitelik | Ne yapar | Örnek |
|---|---|---|
hx-get / hx-post | İstek gönderir | hx-get="/yazilar" |
hx-trigger | İsteği tetikleyen olay | hx-trigger="input changed delay:300ms" |
hx-target | Yanıtın yerleşeceği öğe | hx-target="#sonuclar" |
hx-swap | Yerleştirme biçimi | hx-swap="innerHTML" |
hx-push-url | Adres çubuğunu günceller | hx-push-url="true" |
hx-indicator | İstek sürerken gösterilecek öğe | hx-indicator="#yukleniyor" |
Bu öznitelikler bir araya geldiğinde, bir arama kutusu yalnızca HTML ile ve hiçbir satır JavaScript yazmadan çalışabilir. hx-trigger içindeki changed ve delay değiştiricileri, her tuş vuruşunda istek atılmasını engeller; kullanıcı yazmayı bıraktıktan üç yüz milisaniye sonra istek gider.
<form action="/arama" method="get">
<input
type="search"
name="q"
hx-get="/arama"
hx-trigger="input changed delay:300ms, search"
hx-target="#sonuclar"
hx-push-url="true"
hx-indicator="#yukleniyor"
>
</form>
<p id="yukleniyor" class="htmx-indicator">Aranıyor…</p>
<div id="sonuclar"></div>Dikkat edilmesi gereken bir ayrıntı: hx-get bir form elemanında kullanıldığında, HTMX formun değerlerini isteğin parametresi olarak otomatik ekler. Yani name="q" alanı, sunucuya q olarak ulaşır. Form gönderimini yakalamak için ayrıca bir olay dinleyicisi yazmanız gerekmez.
Bir arama kutusunu uçtan uca kurmak
Gerçek bir uygulamada bu parçaları bir araya getirmek için dört adım yeterli. Önce sunucuda hem tam sayfa hem de yalnızca sonuç parçasını döndüren tek bir uç nokta düşünün. İsteğin HTMX'ten gelip gelmediğini anlamak için HX-Request başlığı kullanılır.
- Yönlendirmeyi ve denetleyiciyi tek bir rota üzerine kurun. Parça ile tam sayfa aynı veriyi kullanmalı, ama farklı görünüm dosyası döndürmeli.
- Sonuç listesini ayrı bir Blade parçasına taşıyın. Bu parça hem tam sayfanın içinde hem de HTMX yanıtı olarak kullanılır.
- Arama kutusunu ve sonuç alanını HTML'de bağlayın; hedef öğenin kimliği ile yanıtın sarmalayıcısı aynı olmalı.
- Testlerde iki durumu da doğrulayın: normal istek tam sayfa,
HX-Requestbaşlıklı istek yalnızca parça döndürmeli.
public function search(Request $request): View
{
$posts = Post::query()
->search($request->string('q')->toString())
->latestFirst()
->get();
return view(
$request->hasHeader('HX-Request') ? 'partials.results' : 'search',
['posts' => $posts],
);
}Bu kodun iki tarafı da aynı veriyi kullanır, bu yüzden arama mantığı tek yerde kalır. Ekran tarafında ise sunucu parçanın nasıl görüneceğini belirler. Bir kart tasarımını değiştirdiğinizde iki görünüm de aynı anda güncellenir.
Testler bu sözleşmeyi güvence altına alır. Bir istek HX-Request başlığı ile gelirse yanıtta <html> etiketi olmamalıdır; başlık yoksa tam sayfa dönmelidir. Bu basit kontrol, ileride biri şablonu yanlışlıkla değiştirdiğinde sessiz bir hatayı yakalar.
Adres çubuğu ve geri tuşu
Bir liste filtrelendiğinde tarayıcının adres çubuğu eskisi gibi kalırsa, kullanıcı sonucu paylaşamaz, sayfayı yenileyemez ve geri tuşu bambaşka bir ekrana götürür. hx-push-url bu sorunu çözer: her başarılı istekte adres çubuğu yanıtın URL'sine güncellenir ve geçmişe bir kayıt eklenir.
Burada bir tuzak var. Adres çubuğu güncellenirse, sayfayı doğrudan o adresten açan biri de aynı sonucu görmelidir. Yani sunucu, /arama?q=laravel adresini tam sayfa olarak da döndürebilmelidir. Yukarıdaki örnekte bu zaten sağlanıyor. Yalnızca parça döndüren uç noktalar bu şekilde paylaşılabilir bir durum üretmez.
| Durum | Adres çubuğu | Geri tuşu |
|---|---|---|
Yalnızca hx-get | Değişmez | Önceki sayfaya döner |
hx-push-url="true" | Yanıtın adresine güncellenir | Önceki filtre durumuna döner |
hx-replace-url="true" | Güncellenir, geçmişe kayıt eklenmez | Bir önceki sayfaya döner |
Filtrelerde hx-replace-url genellikle daha doğru bir tercih: kullanıcı her harfte bir geçmiş kaydı oluşturmak yerine, yalnızca anlamlı bir durum değişikliğinde geri dönebilir.
Hata yanıtlarını ele almak
Form doğrulama hataları HTMX'in en çok yanlış anlaşılan kısmıdır. Varsayılan olarak HTMX, 4xx ve 5xx yanıtlarını sayfaya yerleştirmez. Yani sunucu 422 döndürür ve içinde hata mesajlarıyla birlikte formun yeniden çizilmiş hâlini gönderirse, bu yanıt ekrana hiç yansımaz. Kullanıcı bir şey olmadığını düşünür.
Bunu çözmenin temiz yolu, hata yanıtlarını bilinçli olarak yerleştirmektir. HTMX bir olay yayar; bu olayın içinde yanıt kodunu kontrol edip yerleştirmeye izin verebilirsiniz. Projede bunu tek bir yerde, genel bir dinleyici ile yapmak genellikle yeterlidir.
document.body.addEventListener('htmx:beforeSwap', (event) => {
if (event.detail.xhr.status === 422) {
event.detail.shouldSwap = true;
event.detail.isError = false;
}
});Bu yaklaşımın sınırı açık olmalı. Her 422 yanıtının yerleştirilmesi, beklenmeyen bir hata sayfasının da form alanına yerleşmesine yol açabilir. Yalnızca form doğrulama yanıtlarını, örneğin bir başlık veya işaretle ayrılmış yanıtları yerleştirin ve diğer hata türlerini ayrı ele alın.
Durum tutmak ve Alpine ile iş bölümü
HTMX sunucudaki durumu güçlü tutar. Ancak tarayıcıya ait küçük durumlar da vardır: bir menünün açık olup olmadığı, bir sekmenin hangisinin seçili olduğu, bir modalın görünürlüğü. Bunları sunucuya göndermek gereksiz bir gidiş-dönüş demektir. Alpine.js bu noktada iyi bir eş: yalnızca tarayıcının bildiği durumu yönetir, sunucuyla konuşmaz.
Pratikte ayrım basit bir soruyla yapılır. Bu bilgi sunucuya gitmeli mi? Ürün fiyatı, stok durumu, yorum listesi gitmeli. Menü açık mı, hangi sekme seçili, arama kutusu odakta mı gitmemeli.
| Davranış | Araç | Neden |
|---|---|---|
| Yorum listesi, arama sonuçları | HTMX | Sunucunun doğruluk kaynağı |
| Açılır menü, akordeon, sekme | Alpine.js | Yalnızca tarayıcının durumu |
| Form gönderimi ve yanıtı | HTMX | Sunucu doğrulama yapar |
| Modal açma-kapama | Alpine.js | Anlık geri bildirim gerekir |
Bu bölünme bir kural değil, bir yön gösterici. Ama sınırı net tutmak, ekibin aynı davranışı iki farklı yerde yazmasını engeller.
Erişilebilirlik: değişikliği duyurmak
Sayfa yenilenmediğinde ekran okuyucu kullanıcısı yeni içeriği duymaz. Sonuçlar değişti, ama odak hâlâ arama kutusunda ve okuyucu sessiz. Bu, HTMX'in kendiliğinden çözmediği bir sorun.
Çözüm, sonuç alanına bir canlı bölge eklemek. aria-live="polite" özniteliği, değişiklikleri kullanıcıyı kesmeden okuyucuya bildirir. Sonuç sayısını da bu alanın içinde vermek, kullanıcının ne olduğunu anlamasını kolaylaştırır.
- Sonuç alanına
aria-live="polite"ekleyin. - Sonuç sayısını görünür ve okunabilir bir metinle verin: "12 yazı bulundu".
- Bekleme göstergesini
aria-busyile işaretleyin ve görsel göstergeyi de yardımcı teknolojilere açıklayın. - Klavye ile yapılan filtrelemelerde odağın kaybolmadığını test edin.
Nerede zorlanırsınız?
HTMX her sorunu çözmez, bazı durumlarda kendi sınırlarını hızla gösterir. Birincisi, yoğun etkileşimli araçlar. Sürükle-bırak, çok adımlı form, gerçek zamanlı çizim gibi şeylerde istemci tarafı durum kaçınılmaz hâle gelir ve HTMX bu işi tek başına taşımaz.
İkincisi, aşırı parçalanma. Her küçük değişiklik için ayrı bir uç nokta ve ayrı bir parça yazarsanız, rota sayısı hızla büyür. Bir parçanın kapsamını sayfanın anlamlı bir bölümüyle sınırlamak bu karmaşayı önler.
Üçüncüsü, hedef seçimi. hx-target yanlış öğeye işaret ettiğinde hata çoğu zaman sessizdir: içerik yanlış yere yazılır ama konsolda uyarı çıkmaz. Hedef kimliklerini bir sözleşme gibi düşünün ve bir bileşen değiştirildiğinde kimliklerin korunduğunu kontrol edin.
İyi bir HTMX arayüzünde sunucu neyin nerede duracağına karar verir; tarayıcı yalnızca bunun gerçekleşmesini sağlar.
Bir form doğrulamasını baştan sona kurmak
Bir yorum formu düşünün. Kullanıcı boş bir yorum göndermeye çalışıyor, sunucu bunu reddetmeli ve formu hatalarla birlikte yeniden çizmeli. HTMX ile bunu yapmanın en öngörülebilir yolu, doğrulamayı açıkça yazıp hata durumunda bir parça döndürmektir. Laravel'in varsayılan davranışı burada bir tuzak: JSON beklemeyen isteklerde doğrulama başarısız olduğunda kullanıcıyı önceki sayfaya geri yönlendirir. HTMX isteği bu yönlendirmeyi takip eder ve dönen tam sayfayı hedefe yerleştirmeye çalışır. Sonuç, form alanında beklenmeyen bir sayfa parçası olur.
Bu yüzden parça uçlarında doğrulamayı elle yapmak ve hata durumunda açıkça 422 dönmek daha güvenlidir:
public function store(Request $request, Post $post): Response
{
$validator = Validator::make($request->all(), [
'body' => ['required', 'string', 'min:3', 'max:2000'],
]);
if ($validator->fails()) {
return response()->view('partials.comment-form', [
'post' => $post,
'errors' => $validator->errors(),
'old' => $request->only('body'),
], 422);
}
$post->comments()->create([
'author_name' => $request->user()?->name ?? 'Misafir',
'body' => $request->string('body')->toString(),
]);
return response()->view('partials.comment-success');
}Form parçası, hataları alanın hemen yanında ve aria-describedby ile bağlı olarak göstermelidir. Böylece hem görme hem de ekran okuyucu kullanıcısı aynı bilgiye ulaşır. Parçanın içinde eski değerin de geri yazılması, kullanıcının yazdığı metni kaybetmemesini sağlar.
Burada bir ayrıntı daha var: 422 yanıtının yerleştirilmesi için önceki bölümde anlatılan htmx:beforeSwap dinleyicisi gerekir. Bu dinleyiciyi yalnızca form parçalarını tanıyacak şekilde daraltmak, başka bir hatanın form alanına yerleşmesini de önler.
Sonsuz liste ve "daha fazla yükle"
Uzun bir liste için sayfalama yerine kademeli yükleme, okuma akışını kesintisiz tutar. HTMX bunu tek bir sentinel öğesiyle kurar: öğe ekranda göründüğünde (revealed olayı) bir sonraki parçayı ister ve kendisini bu parçayla değiştirir.
<div id="sonraki-sayfa"
hx-get="/yazilar?sayfa=2"
hx-trigger="revealed"
hx-swap="outerHTML">
Yükleniyor…
</div>Bu desen çalışır, ama iki sınırı vardır. Birincisi, kullanıcı çok hızlı kaydırırsa aynı sentinel birden fazla istek tetikleyebilir; sunucu bu durumda aynı sayfayı iki kez döndürebilir. İkincisi, klavye kullanıcıları için sayfanın sonunda bir "daha fazla yükle" düğmesi de bulunmalıdır. Sonsuz kaydırma yalnızca fareyle çalışan bir özellik olarak kalırsa, klavye ile okuyan biri listenin sonuna hiç ulaşamayabilir.
- Sentinel öğesine odaklanılabilir bir düğme ekleyin ya da yedek bir bağlantı sunun.
- Her parçanın kendi sonraki sayfa bağlantısını taşımasını sağlayın; istemci sayfa numarasını hesaplamasın.
- Yeni yüklenen öğelerin sayısını canlı bir bölgede duyurun.
Testler: istek sözleşmesini doğrulamak
HTMX ile çalışan bir uç nokta, iki farklı çıktı üretir: tam sayfa ve parça. Bu sözleşme bozulduğunda hata sessiz kalır, çünkü sayfa yine de bir yanıt döner. Bu yüzden her iki durumu da test etmek önemlidir.
public function test_search_returns_partial_for_htmx_requests(): void
{
$response = $this->withHeaders(['HX-Request' => 'true'])
->get(route('blog.search', ['q' => 'laravel']));
$response->assertOk();
$response->assertDontSee('<html', false);
$response->assertSee('id="sonuclar"', false);
}İkinci test, tam sayfa isteğinin hâlâ çalıştığını doğrular. Bu çift, hem parça döndüren kodun hem de adres çubuğundan doğrudan açılan sayfanın aynı veriyi gösterdiğini güvence altına alır.
HTMX'in uygun olmadığı durumlar
Her etkileşim HTMX için uygun değildir. Bir çizim tuvali, sürükle-bırak ile sıralanan uzun bir liste ya da her tuş vuruşunda anlık hesaplama gerektiren bir form, istemci tarafında yaşamayı hak eder. Bu tür durumlarda sunucuya her değişikliği göndermek, hem gecikmeyi hem de karmaşıklığı artırır.
Karar verirken basit bir sınama yapın: kullanıcı bu değişikliği bekleyip beklemediğini hissedecek mi? Hiç beklemeden değişen bir şey, HTMX'in alanıdır. Her gidişte beklenen ve sunucunun gerçekten bilmesi gereken bir şey de öyle. Arada kalanları Alpine.js ile yerel tutmak genellikle en dengeli sonuç verir.
Sonuç ve kısa bir kontrol listesi
Sayfa yenilenmeden çalışan bir arayüz kurmak için bir çerçeve yazmanız gerekmiyor. Bağlantılar, formlar ve HTML parçaları yeterli. HTMX bu temel davranışları bir araya getirir, geri kalan işi de sunucuya bırakır. Yeni bir özellik eklemek çoğu zaman yeni bir şablon parçası ve küçük bir öznitelik demek olur.
Başlamadan önce şu soruları sorun:
- Bu veri sunucudan gelmeli mi, yoksa tarayıcının kendi durumu mu?
- Filtre sonucu paylaşılabilir bir adrese sahip olmalı mı?
- Hata yanıtları nasıl yerleşecek ve bunu kim test edecek?
- Değişiklik ekran okuyucuya duyuruluyor mu?
Bu dört soruya cevap verebiliyorsanız, HTMX ile kurduğunuz arayüz büyüdükçe de anlaşılır kalır. Sihir yok; yalnızca her şeyin nerede yaşadığını bilmek var.