Diagnosis ini tidak dibuka oleh laporan bug. Pada 1 September 2026 klien bertanya apakah halaman klaim di direktori mereka sudah siap dipakai pengguna luar untuk mengklaim profilnya, sebuah pertanyaan kesiapan, bukan keluhan. Direktori itu memuat profil orang dan profil perusahaan, dan orang atau perusahaan di baliknya bisa mengklaim profil masing-masing lewat sebuah wizard. Tiga putaran pemeriksaan adversarial ditambah grep saya sendiri menyangkal jawaban optimistisnya. Satu dari empat cacat klaim yang akhirnya saya perbaiki malam itu ternyata cuma angka 300 yang tertulis di dua baris query.
Angka itu sebenarnya sudah tercatat sejak pertengahan Agustus. Pengukuran read-only pada 16 dan 17 Agustus, yang angka penentunya diverifikasi ulang sesudahnya, mendapati lebih dari 400 dari sekitar 700 perusahaan tidak bisa menemukan dirinya sendiri di wizard klaim, karena pencariannya dibatasi 300 baris per tipe. Itu lebih dari separuh perusahaan di direktori. Jumlah persisnya sengaja saya bulatkan di sini.
Dua baris query dan satu urutan abjad
Langkah pertama wizard, FIND, merender daftar orang dan perusahaan ke halaman, lalu membiarkan pengunjung mengetik nama untuk menyaringnya. Sebelum perbaikan, daftar itu diambil dengan dua panggilan ini. Nama post type-nya saya samarkan, sisanya apa adanya:
$people = get_posts(['post_type' => 'dir_person', 'posts_per_page' => 300, 'orderby' => 'title', 'order' => 'ASC']);
$companies = get_posts(['post_type' => 'dir_company', 'posts_per_page' => 300, 'orderby' => 'title', 'order' => 'ASC']);Dibaca sendiri-sendiri, tidak ada yang mencurigakan. posts_per_page membatasi jumlah baris, dan orderby title dengan ASC mengurutkan dari A ke Z. Digabung, artinya jadi sangat spesifik: ambil 300 judul pertama menurut abjad, lalu lupakan sisanya. Batas seperti ini tidak menipiskan daftar secara merata. Dia memotong ekornya, dan pada daftar yang terurut abjad, ekor itu adalah huruf-huruf terakhir.
Batasnya berlaku per tipe. Pengukuran produksi 14 Agustus mencatat profil orang yang terbit belum sampai sepuluh, jauh di bawah 300, jadi yang terpotong hanya daftar perusahaan. Daftar perusahaan berisi sekitar 700 baris, lebih dari dua kali batasnya. Potongannya jatuh di sebuah nama berawalan K, sehingga setiap perusahaan dari huruf K ke bawah tidak bisa ditemukan.
Yang tidak dirender tidak bisa dicari
Angka 300 itu jauh lebih mahal daripada kelihatannya karena cara kerja pencariannya. Kotak pencarian di langkah FIND tidak bertanya ke server. Skripnya hanya mencocokkan substring huruf kecil terhadap atribut data milik baris yang sudah ada di halaman. Bentuk yang disederhanakan, dengan nama atribut disamarkan:
// disederhanakan dari fungsi pencarian di langkah FIND
var q = (input.value || '').trim().toLowerCase();
var visible = 0;
items.forEach(function (it) {
var txt = it.getAttribute('data-search-text') || '';
var show = txt.indexOf(q) !== -1;
it.style.display = show ? '' : 'none';
if (show) visible++;
});
if (empty) empty.style.display = (visible === 0) ? 'block' : 'none';Pesan commit perbaikannya merumuskan akibatnya dengan tepat. Karena pencarian menyaring di sisi klien atas apa yang sudah dirender, batas itu tidak pernah menjadi pengaturan bobot halaman, melainkan penentu siapa yang boleh menemukan dirinya. Bagi perusahaan di urutan ke-301 menurut abjad, masalahnya bukan pencocokan yang meleset. Barisnya memang tidak pernah ada di DOM, jadi ketikan apa pun tidak akan memunculkannya.
Yang terjadi sesudah hasil kosong justru lebih buruk daripada hasil kosong itu sendiri. Kalau tidak ada satu baris pun yang cocok, skrip menampilkan blok kosong, dan sebelum perbaikan blok itu menyatakan bahwa pengunjung belum terdaftar, lengkap dengan tombol untuk meminta profil. Di bawah kotak pencarian juga selalu ada tautan ke formulir profil baru untuk siapa saja yang tidak menemukan dirinya. Jadi perusahaan yang namanya jatuh setelah titik potong, begitu mengetik namanya sendiri, diberi tahu bahwa mereka belum terdaftar lalu diarahkan ke formulir permintaan profil. Formulir itu membuat baris duplikat alih-alih mengklaim baris yang sebenarnya sudah ada.
Handler formulir itu sebenarnya punya penjaga duplikat. Nama yang diketik diubah jadi slug dengan sanitize_title(), dicari dengan get_page_by_path(), dan kalau cocok dengan profil berstatus publish, pengunjung dikembalikan dengan req_error bernilai exists. Komentar di atasnya bahkan menulis bahwa untuk nama yang sudah terdaftar, pintu yang benar adalah klaim, bukan profil duplikat. Tapi pencocokannya persis pada slug, jadi penjaga itu hanya menangkap nama yang slug-nya identik dengan slug profil yang sudah ada. Sementara itu, wizard-nya sendiri sudah lebih dulu meyakinkan pengunjung bahwa mereka belum terdaftar.
Tautan dari halaman profil tidak ikut terpotong
Ada satu hal yang membatasi kerusakannya. Tautan langsung dari halaman profil melewati batas itu, jadi yang terganggu hanya wizard. Di templat klaim, URL yang profil sasarannya dikenali langsung merender langkah 2 dan 3, sedangkan daftar FIND hanya dirender di cabang else. Baris perusahaan yang belum diklaim di daftar itu pun cuma tautan ke URL klaim yang dibangun dari slug profil dan tipenya. Daftar FIND pada dasarnya penunjuk jalan ke URL klaim yang tepat, dan batas 300 menentukan URL siapa saja yang ikut tercetak di halaman.
Batasnya dibuang, ongkosnya diukur
Perbaikannya tidak menaikkan angka. Kedua query sekarang memuat semua baris berstatus publish tanpa batas, tetap terurut menurut judul dari A ke Z:
$people = get_posts(['post_type' => 'dir_person', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true]);
$companies = get_posts(['post_type' => 'dir_company', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true]);Nilai -1 pada posts_per_page berarti tanpa batas. no_found_rows membuat WordPress tidak menghitung total baris untuk keperluan paginasi, yang memang tidak dibutuhkan halaman yang merender semuanya sekaligus.
Merender semuanya jelas ada ongkosnya, dan ongkos itu diukur, bukan diasumsikan: 893 KB mentah, tapi hanya 37,6 KB lewat jaringan dengan gzip, dan render 0,46 detik. Perbandingannya kira-kira 24 banding 1, karena 893 dibagi 37,6 sekitar 23,8. Pesan commit-nya juga mencatat bahwa tidak ada lookup attachment, dan meta serta term tiap baris sudah di-prime oleh get_posts(). Sesudah perbaikan, lebih dari 700 baris dirender.
Perbaikannya di-commit pada malam 1 September 2026. Sesudah deploy, cache WP Rocket dibersihkan, Cloudflare di-purge, lalu hasilnya diperiksa dalam keadaan logout di peramban sungguhan. Di peramban, pencarian memunculkan dua nama dari bagian yang dulu terpotong ditambah nama tempat batas lama jatuh, masing-masing menaut ke URL klaim yang benar. Keempat cacat klaim dari diagnosis hari itu tercatat sudah diperbaiki dan diverifikasi di edge malam itu juga.
Bentuk yang sama di indeks blog situs lain
Dua minggu sebelumnya, pada 18 Agustus, saya memperbaiki mekanisme yang sama di indeks blog sebuah situs lain, dan pemicunya juga bukan laporan bug. SEO lead klien meminta tautan ke arsip salah satu kategori, dengan nama kategori itu sebagai anchor. Yang diminta adalah tautan, bukan perbaikan batas.
Pesan commit perbaikannya membantah premis itu: penyebabnya batas, bukan tautan yang hilang. home.php memanggil get_categories() dua kali, sekali untuk deretan chip filter dan sekali untuk baris konten per kategori, dan keduanya memakai number => 12. Di berkas aslinya kedua panggilan itu berjauhan. Di bawah ini saya rapatkan bersama versi sesudah perbaikan, dengan nama variabel bersamanya disamarkan:
// sebelum: dua daftar, masing-masing dipotong di 12
$wp_categories = get_categories(['hide_empty' => true, 'number' => 12]);
$row_categories = get_categories(['hide_empty' => true, 'number' => 12]);
// sesudah: satu daftar tanpa batas, dibaca oleh keduanya
$categories = get_categories(['hide_empty' => true]);
$wp_categories = $categories;
$row_categories = $categories;Ada 13 kategori yang tidak kosong, dan urutannya menurut nama. Dengan batas 12, kategori yang namanya jatuh paling akhir menurut abjad terbuang dari deretan chip sekaligus dari baris konten, padahal isinya 35 post terbit.
Dari sisi SEO, hilangnya chip itu memang terbaca sebagai tautan yang hilang. Chip kategori di indeks blog adalah anchor sungguhan ke arsip masing-masing kategori, dan ke-13 arsipnya menjawab 200. JavaScript inline memanggil preventDefault() lalu menyaring baris di tempat, tapi crawler tidak menjalankan handler klik. Anchor chip itu satu-satunya tautan internal yang didapat arsip kategori dari indeks blog, jadi kategori yang chip-nya terpotong tidak ditautkan sama sekali dari halaman itu.
Chip tanpa baris mengosongkan halaman
Menaikkan 12 menjadi 13 akan cukup untuk hari itu, dan menambahkan chip-nya saja juga terlihat cukup. Yang kedua justru akan menjadi bug baru. Begitu chip selain all diklik, skrip filter menyembunyikan semua blok dasar di halaman, lalu hanya menampilkan baris yang atribut kategorinya sama dengan term_id chip tersebut. Chip yang tidak punya baris pasangan menyaring halaman sampai kosong.
Perbaikannya berupa satu variabel kategori bersama tanpa batas, dibaca oleh deretan chip dan baris konten sekaligus, sehingga keterikatan keduanya menjadi struktural. Batasnya dibuang, bukan dinaikkan, karena kategori berikutnya yang ditambahkan siapa pun akan menabrak dinding yang sama, dengan cara yang sama, tanpa suara.
Catatan SEO saya menaruh permintaan tautan itu di blok 18 Agustus yang judulnya menyatakan permintaan hari itu semuanya sudah dikirim dan diverifikasi di edge, dengan status DONE. Di luar pernyataan itu dan ke-13 arsip yang menjawab 200, catatan saya tidak memuat angka pengukuran sesudah deploy yang khusus menyebut chip atau baris kategori tersebut.
Batas di daftar terurut adalah potongan, bukan sampel
Kedua kasus ini dibangun dari tiga bahan yang sama: angka batas yang terlihat seperti pengaturan bobot halaman, urutan menurut nama, dan sesuatu di hilir yang menganggap daftarnya lengkap. Pencarian sisi klien menganggap semua kandidat ada di DOM. Filter chip menganggap setiap chip punya baris. Tidak ada yang melempar error, dan yang hilang selalu ekor abjadnya. Ekor itu bertambah panjang setiap kali daftarnya bertambah, karena batasnya tetap.
Kalau ingin menyisir tema sendiri, pencarian kasar seperti ini cukup untuk mengumpulkan kandidatnya:
grep -rnE "'(posts_per_page|numberposts|number)'[[:space:]]*=>[[:space:]]*[0-9]+" wp-content/themes/Hasilnya biasanya bercampur dengan loop arsip biasa yang memang wajar dibatasi, jadi tiap baris tetap perlu dibaca dengan dua pertanyaan. Apakah daftar ini menjadi bahan pencarian atau filter di sisi klien? Dan apakah ada daftar lain yang harus selalu sepasang dengannya? Kalau jawaban pertama ya, angka itu sedang memutuskan siapa yang boleh ada, dan ongkos merender semuanya layak diukur sebelum batasnya dipertahankan. Kalau jawaban kedua ya, kedua daftar itu sebaiknya dibaca dari satu variabel, seperti di indeks blog tadi.