Selain alamat dan isi, setiap request dan balasan HTTP membawa header: baris-baris Nama: nilai yang menerangkan pesan itu. htmx menempelkan beberapa header sendiri di setiap request, dan server bisa membalas dengan header yang memberi perintah ke htmx. Bayangin nota titipan di Laundry Bu Rini. Kasir menulis di pojok nota "dari meja depan, untuk papan nomor 12" (header request). Bu Rini mengembalikan nota dengan kertas tempel "taruh di rak lain" atau "ganti papannya" (header balasan).
🧪 Tebakan cepat: Tombol
<button id="cek-12" hx-get="/order/12/status" hx-target="#status-12">diklik. HeaderHX-Targetyang dikirim htmx berisi#status-12ataustatus-12? Jawabannya ada di bagian "Header yang dikirim htmx".
Header yang dikirim htmx
Klik Cek status di baris #12 mengirim request ini:
GET /order/12/status HTTP/1.1
HX-Request: true
HX-Trigger: cek-12
HX-Target: status-12
HX-Current-URL: http://localhost:8000/papan
| Header | Isinya | Kapan ada |
|---|---|---|
HX-Request |
true |
setiap request htmx |
HX-Trigger |
id elemen pemicu, cek-12 |
kalau pemicunya punya id |
HX-Trigger-Name |
name elemen pemicu, misalnya cari |
kalau pemicunya punya name |
HX-Target |
id target, status-12 |
kalau target-nya punya id |
HX-Current-URL |
alamat halaman sekarang | setiap request htmx |
HX-Boosted |
true |
link atau form yang di-boost |
HX-Target berisi id saja, tanpa #: status-12. Itu jawaban tebakannya. Saat form dikirim, HX-Trigger berisi id form-nya (terima), bukan tombolnya, karena yang memicu submit adalah form.
hx-headers: header tambahan
<button hx-get="/api/data" hx-headers='{"X-Custom": "my-value"}' hx-target="#result">
Load with Headers
</button>
- Isinya JSON. Bungkus atributnya dengan kutip satu, karena JSON wajib memakai kutip dua di dalamnya.
- Request-nya membawa
X-Custom: my-valuedi samping header htmx. - JSON yang salah, misalnya
{'X-Custom': 'my-value'}, membuat htmx error di Console dan request-nya gak dikirim. hx-headersdiwariskan. Di Laravel, satu atribut di<body>menempelkan token CSRF ke setiap POST dan DELETE:<body hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'>.
Satu route, dua bentuk balasan
Server bisa membaca HX-Request untuk memilih balasan: potongan HTML untuk htmx, halaman utuh untuk browser biasa.
Route::get('/papan', function (Request $request) {
$orders = Order::where('nama', 'like', '%'.$request->query('cari').'%')->get();
$view = $request->header('HX-Request') ? 'papan._daftar' : 'papan.index';
return response()->view($view, ['orders' => $orders])
->header('Vary', 'HX-Request');
});
GET /papan?cari=dartodari kolom cari (membawaHX-Request: true) dibalas beberapa<li>.- Alamat yang sama dibuka langsung di tab baru dibalas halaman papan lengkap.
Vary: HX-Request: lihat bagian berikutnya.
Vary: HX-Request untuk cache
Browser (dan CDN) menyimpan balasan di cache dengan kunci alamatnya. Tanpa Vary, /papan?cari=darto versi potongan <li> dan versi halaman utuh berebut satu tempat. Kasir mencari "darto", membuka Laporan lewat link biasa, lalu menekan Back: browser mengambil /papan?cari=darto dari cache dan mendapat potongan <li> dari request htmx tadi. Yang tampil daftar polos tanpa layout, tanpa kolom cari.
Vary: HX-Request membuat cache menyimpan dua versi, satu per nilai HX-Request. Back tanpa HX-Request mendapat halaman utuh. Satu hal lagi: saat riwayat htmx gak ada di cache-nya sendiri, htmx 2.0.11 meminta halaman utuh dengan HX-Request: true. Pasang <meta name="htmx-config" content='{"historyRestoreAsHxRequest": false}'> supaya request itu gak dikira request potongan.
Header balasan: server memberi perintah
| Header balasan | Yang dilakukan htmx | Contoh di papan |
|---|---|---|
HX-Trigger |
memicu event di client (pelajaran events) | orderBaru sesudah order dicatat |
HX-Redirect |
pindah halaman penuh ke alamat itu | Tutup kasir, lalu ke /laporan |
HX-Refresh |
true: muat ulang seluruh halaman |
harga naik dari Rp 7.000 jadi Rp 8.000 per kg |
HX-Retarget |
ganti target dengan selector ini | order dobel: tukar baris #order-14 yang sudah ada |
HX-Reswap |
ganti cara swap | outerHTML untuk baris itu |
HX-Push-Url |
taruh alamat ini di address bar dan riwayat | /papan?cari=darto sesudah mencari |
// Order dobel: perbarui baris yang sudah ada, bukan menambah baris baru
return response()->view('papan._baris', ['order' => $order])
->header('HX-Retarget', '#order-14')
->header('HX-Reswap', 'outerHTML');
// Tutup kasir: pindah ke halaman laporan
return response('', 200)->header('HX-Redirect', '/laporan');
HX-RetargetdanHX-Reswapcuma berlaku untuk balasan yang memang ditukar. Balasan 422 tetap gak ditukar walau membawaHX-Retarget.- Jangan pakai
redirect('/laporan')biasa untuk request htmx. Status 302 diikuti XHR sendiri secara diam-diam, dan htmx menaruh halaman Laporan utuh di dalam#papan-order: halaman di dalam halaman.HX-Redirectyang benar.
⚠️ Jebakan yang sering ketemu
- JSON
hx-headerssalah kutip.hx-headers="{'X-Custom': 'my-value'}"gak valid, dan request-nya gak dikirim. Di latihan, tombolnya butuhhx-get="/api/data"danhx-headers='{"X-Custom": "my-value"}'. - Mencari
#diHX-Target. Isinyastatus-12, tanpa#, dan kosong kalau target-nya gak punya id. - Satu alamat, dua bentuk balasan, tanpa
Vary: HX-Request. Back atau CDN menyajikan potongan<li>sebagai halaman. - Redirect 302 untuk request htmx. Halaman tujuan masuk ke dalam target. Kirim
HX-Redirect.
TL;DR: htmx mengirim HX-Request: true di setiap request, plus HX-Trigger (id pemicu), HX-Trigger-Name (name pemicu), HX-Target (id target, tanpa #), HX-Current-URL, dan HX-Boosted untuk yang di-boost. hx-headers='{"X-Custom": "my-value"}' menambah header sendiri (JSON, kutip satu di luar), dan diwariskan, misalnya token CSRF di <body>. Server membaca HX-Request untuk membalas potongan atau halaman utuh, dan wajib menambah Vary: HX-Request supaya cache gak tertukar. Header balasan memberi perintah: HX-Trigger (event), HX-Redirect (pindah halaman penuh), HX-Refresh (muat ulang), HX-Retarget dan HX-Reswap (ganti target dan cara swap), HX-Push-Url (address bar). Untuk request htmx, pakai HX-Redirect, bukan redirect 302.