Email dari klien itu memuat tiga pengamatan tentang backend, dan salah satunya berbunyi begini: artikel yang baru terbit tidak langsung muncul di homepage, di daftar terbaru, maupun di search. Tiga permukaan sekaligus dalam satu kalimat keluhan, dan ternyata bukan satu penyebab.
Bagian homepage selesai lebih dulu, di ronde kerja sebelumnya, dan tanpa satu baris kode. Penyebabnya bukan cache. Customizer tema ini punya allowlist penulis untuk feed, dan allowlist itu sudah mencentang beberapa user tapi tidak mencentang penulis yang mengeluh. Begitu ada satu saja yang dicentang, hanya penulis di daftar itu yang tampil di feed. Empat post terbarunya, yang terbit berurutan dalam rentang belasan menit di hari yang sama, dikeluarkan diam-diam dari homepage padahal mereka yang paling baru. Saya curl halaman depannya dan mencari judul-judulnya: nol kecocokan. Perbaikannya di setelan saja: buka centang semua user di allowlist feed, lalu centang penulis itu di allowlist halaman blog. Ronde itu tidak mengirim kode sama sekali.
Yang tersisa adalah search, dan di situlah saya sempat puas terlalu cepat.
Diagnosis pertama: SQL search WordPress tidak tahu siapa penulisnya
Di ronde berikutnya laporan search itu saya kerjakan. Gejalanya: mencari nama depan penulis itu mengembalikan tepat satu hasil, yaitu satu artikel yang kebetulan menyebut namanya di badan tulisan, sementara empat artikel terbarunya tidak terlihat sama sekali.
Penjelasan yang saya pegang waktu itu masuk akal dan, sejauh menyangkut SQL, memang benar. Search bawaan WordPress hanya memindai post_title, post_content, dan post_excerpt. Byline penulis hidup di akun user WordPress, bukan di teks badan, jadi query itu memang tidak pernah bisa menemukannya.
Maka saya tulis filter posts_search yang membungkus klausa search bawaan dengan grup OR: post yang post_author-nya cocok dengan user yang namanya mengandung kata kunci, atau post yang meta byline-nya mengandung kata kunci.
AND ( {klausa search bawaan WP} OR (
wp_posts.post_author IN ({id user yang cocok di display_name/login/nicename})
OR wp_posts.ID IN ({id post dengan _theme_byline LIKE %kata%})
))Filter itu dijaga empat kondisi: bukan is_admin(), is_main_query(), is_search(), dan kata kuncinya minimal dua karakter dihitung dengan mb_strlen. Pencarian user-nya lewat get_users dengan pola wildcard, tiga kolom search, hanya mengambil ID, dan dibatasi 50. Pencarian byline-nya query LIKE langsung lewat $wpdb ke meta byline, dengan esc_like dan LIMIT 200.
$user_ids = get_users( [
'search' => '*' . $s . '*',
'search_columns' => [ 'display_name', 'user_login', 'user_nicename' ],
'fields' => 'ID',
'number' => 50,
] );Filter ini berangkat bersama tiga butir lain dari ronde yang sama dalam satu berkas baru di folder inc, sekitar 290 baris, plus satu baris require_once di functions.php. Urutan unggahnya tidak boleh dibalik: berkas inc dulu, baru functions.php. Kalau functions.php naik lebih dulu, require_once menunjuk ke berkas yang belum ada, wp-admin ikut mati karena fatal, dan satu-satunya jalan pulang lewat SFTP.
Di catatan saya hari itu tertulis bahwa pencarian nama penulis sekarang mengembalikan semua artikel yang dia tulis dan semua artikel yang menyebut namanya di meta byline. Kalimat itu ditulis dari membaca kode, bukan dari membuka situsnya.
Verifikasi di browser membalik ceritanya
Setelah filter PHP itu hidup, saya buka situsnya lewat Playwright MCP, dan verifikasi itu memunculkan tiga celah yang tidak terlihat dari audit PHP saja.
Celah paling dasar: tema ini tidak punya search.php di root tema. Struktur temanya mengasumsikan semua render lewat front-page.php, single-*.php, page-templates/*.php, dan template taxonomy, sementara index.php-nya hanya placeholder yang merender satu H1. Ada template pencarian di page-templates, berupa indeks yang bisa dicari di sisi klien, tapi dia hanya jalan kalau ditugaskan manual ke halaman /search/. Kotak search di header mengirim ke URL native ?s=, dan URL itu, lewat hierarki template WordPress, mendarat di hasil yang rusak. Di catatan saya, inilah yang ada di balik laporan "hanya satu hasil" dari klien.
Jadi filter SQL yang saya kirim lebih dulu membungkus query untuk halaman yang tidak pernah merender hasil apa pun. Klausanya benar, hasilnya tidak pernah sampai ke layar.
Celah yang sama menjelaskan gejala lain di situs itu. Tema ini juga tidak punya page.php, sehingga halaman statis yang kontennya lengkap di basis data ikut jatuh ke placeholder yang sama dan tampak kosong.
Celah ketiga ada di indeks search sisi klien itu sendiri. Field byline-nya hanya membaca postmeta byline. Empat post penulis itu tidak punya meta byline, karena dia memang post_author-nya, jadi penilai JS di kotak search mengembalikan nol untuk namanya. Template single sudah punya fallback yang benar, byline meta kalau ada dan nama author WordPress kalau tidak, tapi pembangun indeksnya tidak meniru itu.
Perbaikannya
Hari kedua ronde itu mengirim empat berkas. search.php di root tema, empat baris saja. page.php baru sekitar 38 baris untuk halaman statis. Template-part indeks search yang diekstrak dari page-template lama, dan page-template itu sendiri dipangkas dari sekitar 265 baris jadi 14.
<?php
// search.php di root tema, otomatis dipakai WordPress untuk URL ?s=
get_header();
get_template_part( 'template-parts/search-list' );
get_footer();Di template-part itu, tiga loop yang mengisi indeks (berita, wawancara, blog) memakai satu helper resolve byline dengan fallback ke display_name author, meniru pola yang sudah ada di single template. Input search-nya diisi dari get_search_query(), supaya deep link ?s= langsung memfilter saat halaman dimuat, karena render() memang dipanggil di akhir initSearch().
$resolve_byline = function ( $byline, $post_author ) {
return $byline ?: get_the_author_meta( 'display_name', $post_author );
};<input type="search" name="s" value="<?php echo esc_attr( get_search_query() ); ?>">Ketiga perbaikan naik tanpa rollback kode dan tanpa campur tangan manual di editor. Cache WP Rocket saya bersihkan lewat Playwright MCP dengan satu klik pada tombol clear and preload, dan notice-nya mencatat waktunya. Verifikasi terakhir dilakukan di sisi server tanpa parameter bypass cache: URL search dengan nama penulis itu mengembalikan halaman 800 KB lebih, input sudah terisi nama itu, indeksnya memuat seratusan lebih item, dan filter JS merender empat hasil begitu halaman dimuat. Empat, sama banyaknya dengan artikel yang tadinya hilang.
Kenapa purge cache ikut dikirim
Satu butir lagi dari ronde yang sama menyentuh sisi homepage dari keluhan itu. Setelah allowlist dibuka, post penulis itu seharusnya langsung muncul di homepage, tapi tidak. Dugaan saya waktu itu: WP Rocket otomatis membersihkan URL post yang disimpan, tapi pembangunan ulang cache homepage bisa tertinggal, dan cache homepage yang ada dibuat sebelum publish dengan TTL yang belum habis. Ini dugaan, bukan sesuatu yang saya ukur.
Jadi saya tambahkan purge defensif: satu fungsi yang dikaitkan ke save_post_post, save_post_story, publish_post, dan publish_story dengan prioritas 30. Dia memanggil rocket_clean_home(), rocket_clean_post( $post_id ), dan rocket_clean_files() dengan daftar eksplisit permukaan arsip yang kemungkinan berubah: /, /search/, halaman daftar berita, dan tiap arsip term yang post itu miliki.
Pelajaran
Audit PHP membaca kode yang ada. Dia tidak melihat berkas yang tidak ada, dan hierarki template WordPress justru bekerja lewat ketiadaan berkas: tidak ada search.php, tidak ada page.php, semuanya jatuh ke index.php. Filter posts_search itu tidak salah, dia cuma menjawab pertanyaan yang belum pernah ditanyakan halamannya.
Ronde sebelumnya sudah memberi saya aturannya, dan saya melanggarnya di ronde ini. Untuk bug yang hanya terlihat pengunjung, curl HTML mentahnya sebagai anonim, karena admin yang login melewati cache. Di ronde itu pun saya sempat menebak akar masalah tanpa verifikasi, ditegur, lalu pindah ke diagnosis berbasis curl dan baru menemukan penyebab sebenarnya. Kali ini bentuknya beda tapi polanya sama: kesimpulan yang ditulis dari kode, sebelum halamannya dibuka. Sekarang, kalau keluhan klien berbunyi "search cuma memberi satu hasil", langkah pertama saya bukan membaca query, melainkan membuka URL ?s= yang dia pakai dan melihat template mana yang sebenarnya menjawab.