Di sebuah situs WordPress yang saya rawat, ada chip kategori yang seharusnya menampilkan "BANKING & NEOBANKS". Yang muncul di layar malah ini:
BANKING & NEOBANKS
Teks & literal, di tengah-tengah label. Bukan render karakter &, tapi benar-benar lima karakter: ampersand, A, M, P, titik koma. Kelihatannya seperti kerusakan encoding acak, padahal sebenarnya sangat logis begitu Anda tahu penyebabnya.
Markup-nya sederhana — sebuah chip yang meng-uppercase nama term:
<span class="chip">
<?php echo esc_html( strtoupper( $term->name ) ); ?>
</span>Nama kategorinya "Banking & Neobanks". Jadi kenapa & malah jadi &?
Akar masalahnya: nama term sudah ter-encode di konteks display
Inilah bagian yang membuat saya terjebak. Ketika WordPress menyerahkan field term ke Anda di konteks 'display' (yang terjadi pada get_the_terms, wp_get_post_terms, dan saat term diakses lewat loop biasa), ia melewatkan field itu melalui sanitize_term_field. Dan di konteks 'display', sanitizer itu meng-encode & menjadi &.
Artinya $term->name untuk "Banking & Neobanks" sebenarnya bukan string yang Anda kira. Nilai aslinya di memori adalah:
Banking & Neobanks
Sekarang ikuti alurnya. Kalau Anda hanya esc_html lalu cetak, semuanya baik-baik saja — browser menerima & dan men-decode-nya kembali jadi &. Itulah kenapa kode yang tampak naif sering kali "kebetulan benar".
Tapi begitu Anda menyisipkan strtoupper, kode itu meng-uppercase seluruh string — termasuk entity-nya:
Banking & Neobanks
strtoupper ↓
BANKING & NEOBANKS
Dan & bukan HTML entity yang valid. Entity bersifat case-sensitive; yang valid adalah & (huruf kecil). Karena & tidak dikenali, browser tidak men-decode-nya — ia menampilkannya apa adanya. Itulah sebabnya teks literal muncul di chip.
Kenapa CSS text-transform tidak kena, tapi PHP/JS kena
Saya sempat berpikir untuk uppercase lewat CSS saja, dan itu memang menghindari bug ini — karena alasan yang penting untuk dipahami:
.chip { text-transform: uppercase; } /* aman */text-transform: uppercase bekerja pada teks yang sudah dirender. Pada saat CSS bekerja, browser sudah men-decode & menjadi &, jadi yang di-uppercase adalah karakter & — bukan entity. Hasilnya visual saja, dan benar.
Tapi strtoupper (PHP) dan toUpperCase() (JS) bekerja pada string mentah sebelum browser sempat men-decode apa pun. Mereka melihat & dan dengan patuh meng-uppercase-nya jadi &. Inilah jebakannya: fungsi case di lapisan kode merusak entity, fungsi case di lapisan tampilan tidak.
Perbaikannya: decode entity SEBELUM transformasi case
Solusinya adalah men-decode entity kembali ke karakter mentahnya dulu, baru meng-uppercase:
<span class="chip">
<?php echo esc_html(
strtoupper( wp_specialchars_decode( $term->name, ENT_QUOTES ) )
); ?>
</span>Sekarang alurnya benar:
wp_specialchars_decode( $term->name, ENT_QUOTES )mengubahBanking & Neobanks→Banking & Neobanksstrtoupper( ... )mengubahnya →BANKING & NEOBANKSesc_html( ... )meng-encode ulang&→&dengan aman untuk output- Browser men-decode
&→&saat render
Hasil akhir di layar: BANKING & NEOBANKS. Persis seperti yang diinginkan.
Jebakan yang sama berlaku untuk strtolower, ucfirst, dan mb_strtoupper. Apa pun fungsi case di PHP — atau toUpperCase/toLowerCase di JavaScript — akan merusak entity kalau dijalankan di atas string term mentah.
Pelajaran
Nama term di WordPress datang dalam keadaan ter-encode sebagai HTML entity saat konteks 'display'. $term->name untuk "Banking & Neobanks" sebenarnya adalah "Banking & Neobanks". esc_html saja tidak masalah karena browser men-decode entity kembali — tapi begitu Anda menjalankan strtoupper/strtolower di atasnya, & berubah jadi &/& yang tidak valid dan bocor sebagai teks literal.
Aturannya: jangan pernah jalankan fungsi case PHP/JS di atas nama term tanpa wp_specialchars_decode( ..., ENT_QUOTES ) lebih dulu. Kalau Anda hanya butuh tampilan uppercase, text-transform: uppercase di CSS adalah jalan teraman — itu bekerja pada teks yang sudah ter-decode dan tidak akan pernah menyentuh entity.
