Bug ini bukan warisan siapa-siapa. Saya sendiri yang menulisnya, sadar, sebagai perbaikan untuk bug lain yang waktu itu terasa lebih mendesak di platform pemesanan properti sewa yang saya kerjakan.
Masalah aslinya begini. Cabang yang benar-benar mengambil data dari basis data cuma jalan kalau salah satu dari lokasi, tanggal check-in, atau jumlah tamu terisi. Akibatnya kunjungan polos ke halaman pencarian dan tautan kategori dari beranda melewati cabang itu sepenuhnya, lalu pengunjung mendarat di 18 kartu demo yang semuanya menaut ke satu halaman pratinjau yang sama.
Perbaikan yang saya kirim hari itu menjalankan query tanpa syarat, memetakan slug kategori ke rangkaian kata kunci yang dilempar ke WP_Query sebagai s=, dan membuat cabang kartu demo memutar ID listing asli lewat index % count supaya kartu fallback pun membuka pratinjau yang berbeda-beda.
// slug kategori diterjemahkan jadi kata kunci, lalu dipakai sebagai pencarian teks
$args['s'] = app_search_category_terms($category);Saya mencatat cacatnya di hari yang sama
Di catatan audit rilis yang sama, saya menulis temuan bernomor MEDIUM #14b: pemetaan ?category= memakai keyword s= milik WP_Query, dan s= itu AND-search antar kata, jadi kategori tidak benar-benar mempersempit hasil ke listing kategori itu dan tetap jatuh ke cabang demo. Karena cabang demo sudah memutar ID asli, gejalanya bukan halaman kosong. Kartunya tetap terbuka, cuma judul-judulnya tidak cocok dengan listing yang seharusnya muncul.
Di catatan itu saya juga menulis dua kemungkinan perbaikan sungguhan, meta_query pada kota atau taxonomy khusus, tanpa memutuskan yang mana. Lalu saya tunda ke rilis patch berikutnya. Perbaikannya tidak pernah mendarat di rilis itu.
Kenapa s= tidak bisa dipakai sebagai filter
s= bukan mekanisme filter. Dia pencarian teks bebas ke judul, konten, dan kutipan post, jadi yang dicocokkan cuma prosa yang kebetulan ditulis manusia, bukan data terstruktur.
Yang lebih menipu adalah perilaku multi katanya. s= dengan lebih dari satu kata bersifat AND, bukan OR. WordPress memecah string pencarian jadi kata-kata lalu mensyaratkan semuanya hadir di post yang sama. Artinya makin deskriptif pemetaan kata kuncinya, makin sedikit yang lolos. Menambah kata dengan niat memperluas hasil justru mempersempitnya sampai habis, dan itu kebalikan dari intuisi kebanyakan orang soal kotak pencarian.
Lalu kenapa dulu ini kelihatan bekerja? Penelusuran belakangan menyebutnya apa adanya: kategori itu keyword hack yang kebetulan cocok dengan NAMA listing seed saya sendiri, sehingga listing pemilik asli tidak pernah masuk rail mana pun dan filter kategori yang sungguhan tidak pernah bekerja. Slug enam listing seed itu semuanya nama tempat, dan kosakata itulah yang kebetulan bertemu dengan kata kunci pemetaannya. Enam listing itu memang ada di produksi, isinya konten demo yang saya siapkan untuk pengujian, dan yang menjalankan seed-nya di produksi ternyata klien sendiri, di suatu titik yang tidak diketahui persis.
Yang menghidupkan lagi perkaranya adalah klien, bukan saya
Dua puluh enam hari setelah catatan #14b itu, perbaikan sungguhannya baru dikirim. Pemicunya bukan saya membuka lagi daftar tunda saya. Klien yang menegur: saya mengubah banyak hal dan belum tentu semuanya di-wire, dan dia minta saya memeriksa ulang mana saja yang belum.
Rail kategori di beranda cuma satu contoh yang kebetulan dia lihat, dan dia yakin masih ada yang lain yang tidak terpikir oleh saya untuk disambungkan. Poinnya dia rumuskan sebagai rantai yang putus: banyak bagian sudah diarahkan ke data asli, tapi rantai dari masukan pemilik, ke tersimpan, ke tampil atau terkueri, tidak nyambung ujung ke ujung.
Sebelum batch itu saya sendiri pun belum yakin apakah render awal halaman pencarian benar-benar menangani parameter category, dan saya menandainya sebagai hal yang harus diverifikasi dulu, bukan sebagai sesuatu yang sudah saya ketahui.
Bug kedua dengan bentuk yang sama persis
Penelusuran itu menemukan filter lokasi bernasib serupa. Taxonomy lokasinya ada, tapi tidak pernah diisi oleh form tambah properti, karena form itu cuma punya input teks untuk kota, region, dan negara, tanpa field term lokasi. Parameter lokasi tidak pernah terkirim ke REST, jadi wp_set_object_terms tidak pernah jalan.
Tiga akibatnya tercatat. Filter lokasi di pencarian mem-query taxonomy itu, jadi dia tidak akan pernah cocok dengan listing pemilik. Tampilan lokasi di halaman detail jatuh balik ke alamat karena term-nya kosong. Dan pencocokan lokasi di beranda cuma bersandar pada teks keyword.
Dua bug, satu bentuk: sisi baca sudah dipasang, sisi tulis tidak pernah. Penelusuran yang sama juga menemukan bidang area, kebisingan, tipe properti, kota, region, dan negara yang tersimpan tapi tidak pernah ditampilkan, plus bagian things-to-know yang menanam alarm keselamatan palsu per listing dan satu kebijakan pembatalan yang dipatok.
Tidak semua yang ditandai sweep ternyata benar. Biaya kebersihan sempat masuk daftar curiga, padahal sudah diterapkan di pricing, di bookings, dan di invoice. Satu route pengecekan harga dilaporkan hilang, padahal berkasnya ada di tema, jadi itu false positive. Sweep otomatis bagus untuk menyusun daftar periksa, bukan untuk menyimpulkan.
Bentuk perbaikannya yang memutuskan klien
Waktu #14b ditunda dulu, saya belum memilih antara meta_query pada kota atau taxonomy khusus. Yang akhirnya memutuskan arahnya klien: pertahankan kategori editorial, tapi pemilik listing yang memilihnya.
Dia sekalian memutuskan empat bentuk lain di sesi itu. Kategori boleh lebih dari satu lewat checkbox. Lokasi jadi dropdown region. Fitur keselamatan diisi pemilik per listing. Kebijakan pembatalan ditetapkan platform tapi bisa disunting admin.
Membuat datanya dulu, baru query-nya
Selama kategori bukan data, tidak ada query yang bisa menyelamatkannya.
Langkah pertama, taxonomy datar sungguhan dengan lima term yang ditentukan, rewrite dimatikan, didaftarkan berdampingan dengan tiga taxonomy yang sudah ada di plugin itu.
register_taxonomy('listing_category', 'listing', [
'hierarchical' => false,
'rewrite' => false,
]);Langkah kedua, term-nya harus benar-benar ada di produksi. Penyemaiannya saya kunci sekali jalan lewat sebuah option, supaya produksi tersemai tanpa perlu mengaktifkan ulang plugin.
if (!get_option('listing_category_seeded')) {
foreach (app_category_choices() as $slug => $label) {
if (!term_exists($slug, 'listing_category')) {
wp_insert_term($label, 'listing_category', ['slug' => $slug]);
}
}
update_option('listing_category_seeded', 1);
}Gate itu bukan kerapian, itu keharusan. Saya tidak punya FTP maupun SSH ke produksi, dan rilisnya sampai ke sana lewat unggahan berkas oleh klien. Mengunggah berkas tidak menjalankan ulang activation hook, jadi seeding yang digantung di sana tidak akan pernah jalan. Di lingkungan yang sama, menaikkan konstanta versi cuma membatalkan cache aset lewat ?ver=, tidak membersihkan cache halaman, jadi tiap deploy yang menyentuh copy terender masih butuh purge manual dari klien.
Langkah ketiga, sisi tulis. Form tambah properti dapat checkbox kategori, dan endpoint REST-nya menerima parameter categories[] yang divalidasi terhadap daftar default sebelum menyentuh basis data.
$allowed = array_keys(app_category_choices());
$categories = array_values(array_intersect((array) $request['categories'], $allowed));
wp_set_object_terms($post_id, $categories, 'listing_category', false);Ada satu detail JavaScript yang gampang terlewat. Pengumpulan field bertipe array saya generalkan untuk amenity, kategori, dan fitur keselamatan sekaligus, dan array kosong tetap dikirim, supaya penyuntingan bisa mengosongkan pilihan yang sudah tersimpan. Submit juga ditahan di sisi klien kalau nol kategori terpilih, dengan toast yang bilang kategori wajib dipilih, bukan diam.
Kolom region yang tadinya teks bebas diganti <select name="region" required> berisi term region, yang di taxonomy-nya adalah anak dari term negara.
Langkah keempat, sisi baca. Fungsi yang menyusun data properti sekarang mengembalikan kategori beserta slug-nya, jadi form edit bisa mencentang ulang apa yang tersimpan. Query-nya sendiri dipusatkan di satu helper yang memvalidasi slug sebelum menyusun tax_query.
function app_apply_category_filter(array $args, string $category): array {
$choices = app_category_choices();
if (!isset($choices[$category])) {
return $args; // slug tidak dikenal, tidak ada tax_query yang ditambahkan
}
$args['tax_query'] = [[
'taxonomy' => 'listing_category',
'field' => 'slug',
'terms' => $category,
]];
return $args;
}Perhatikan cabang untuk slug tak dikenal di kode di atas. Mengembalikan argumen apa adanya berarti halaman tampil tanpa filter kategori sama sekali, bukan tampil kosong. Kalau yang diinginkan hasil kosong, cabangnya harus mengatakan itu.
Helper ini dipakai rail beranda, halaman hasil pencarian, dan endpoint load-more, jadi tiga permukaan itu tidak bisa lagi menyimpang satu sama lain. Fungsi kata kunci yang lama saya hapus, bukan saya biarkan menganggur. Satu rail tidak ikut pindah, yaitu rail populer, karena isinya memang berarti listing terbaru, bukan sebuah kategori.
Verifikasi dengan data yang bukan buatan saya
Pengujiannya saya lakukan lewat klik langsung sebagai admin. Taxonomy-nya terdaftar dan lima term-nya tersemai otomatis. Form tambah properti merender checkbox kategori, dropdown region berisi lima opsi, dan checkbox keselamatan, lalu submit sungguhan dari form itu tersimpan dan terisi ulang waktu disunting lagi.
Satu listing uji saya beri dua kategori dan satu region. Hasilnya, halaman pencarian dengan slug kategori pertama mengembalikan dia saja, kategori lain memberi empty state, rail dua kategori itu berisi dia, dan tiga rail sisanya disembunyikan. Fitur keselamatan di halaman detail cuma menampilkan yang dicentang pemilik, dan klaim alarm karbon monoksida karangan yang dulu tertanam itu hilang. Widget booking dan tombol Reserve utuh, galerinya foto asli tanpa kebocoran demo. Listing uji itu sendiri masih menunggu dibersihkan.
Enam listing seed lama tidak punya term kategori sama sekali, jadi rail editorial memang kosong sampai ada listing yang dikategorikan. Itu keadaan yang benar dan jujur.
Pelajaran
s=diWP_Queryitu pencarian teks bebas, dan multi kata di dalamnya bersifat AND. Menambah kata mempersempit hasil, bukan memperluas. Jangan memakainya sebagai pengganti filter.- Kalau sebuah kategori tidak tersimpan sebagai data, filternya bukan filter, dia cuma kebetulan yang sedang beruntung.
- Tambal sulam yang cacatnya sudah kamu catat sendiri tetap tambal sulam. Catatan MEDIUM saya benar isinya, tapi yang membuatnya dikerjakan bukan catatan itu, melainkan teguran orang lain 26 hari kemudian.
- Validasi slug dari query string terhadap allowlist, lalu tentukan dengan sadar apa yang terjadi pada slug yang tidak dikenal. Mengembalikan argumen query apa adanya berarti hasilnya tidak tersaring sama sekali, bukan kosong.
- Periksa fitur per rantai, dari input pengguna, ke penyimpanan, ke pembacaan. Bug paling awet adalah yang setengah tersambung, entah tersimpan tapi tidak pernah ditampilkan, atau di-query tapi tidak pernah ditulis.
- Kalau rilis ke produksi berjalan lewat unggah berkas, jangan menggantung seeding di activation hook. Gate dengan satu option supaya jalan sekali tanpa reaktivasi.
- Tampilan yang penuh bukan bukti fitur bekerja, apalagi kalau isinya data seed buatan sendiri.