Di sebuah situs WordPress yang saya rawat, saya butuh sebuah URL prefix tunggal, misalnya /style/..., yang bisa melayani dua tipe konten: sebuah custom post type lebih dulu, dan kalau tidak ada, fallback ke post biasa. Idenya sederhana di kepala: daftarkan dua rewrite rule dengan regex yang sama, satu untuk CPT, satu untuk post, biarkan WordPress mencoba yang pertama lalu jatuh ke yang kedua.
Hasilnya: semua slug CPT 404. Hanya rule yang terakhir didaftarkan yang bertahan. Tidak ada chaining, tidak ada fallback.
Akar masalah: rewrite rule di-key berdasarkan regex
Inilah bagian yang menjebak. WordPress menyimpan rewrite rules sebagai sebuah array yang di-key oleh string regex-nya. Jadi kalau Anda mendaftarkan dua rule dengan regex identik tapi query string berbeda, yang kedua menimpa yang pertama, persis seperti $arr[$key] = $a; $arr[$key] = $b;. Hanya satu rule per regex unik yang bisa hidup.
Kode yang saya kira benar:
// SALAH — rule kedua menimpa rule pertama, tidak ada fallthrough
add_rewrite_rule( '^style/([^/]+)/?$', 'index.php?post_type=article&name=$matches[1]', 'top' );
add_rewrite_rule( '^style/([^/]+)/?$', 'index.php?name=$matches[1]', 'top' ); // ini yang menangKarena kedua regex identik (^style/([^/]+)/?$), entri kedua menumpuk yang pertama. Yang tersisa hanyalah rule post. Slug yang sebenarnya milik CPT article tidak punya rule yang cocok lagi → 404.
WordPress memang tidak punya konsep "coba rule ini, kalau tidak match coba berikutnya" untuk regex yang sama. Setiap regex hanya boleh muncul sekali.
Solusi: satu rule + private query var + filter request
Karena hanya satu rule per regex yang bisa hidup, jangan lawan itu. Pakai satu rule saja, tandai dengan private query var, lalu mekarkan tanda itu di sebuah filter request menjadi array post_type.
// 1. SATU rule, dengan penanda query var sendiri
add_action( 'init', function () {
add_rewrite_rule(
'^style/([^/]+)/?$',
'index.php?name=$matches[1]&my_multitype=1',
'top'
);
} );
// 2. Daftarkan query var penanda agar WP mengenalinya
add_filter( 'query_vars', function ( $vars ) {
$vars[] = 'my_multitype';
return $vars;
} );
// 3. Mekarkan penanda menjadi array post_type di filter request
add_filter( 'request', function ( $qv ) {
if ( ! empty( $qv['my_multitype'] ) ) {
$qv['post_type'] = array( 'post', 'article' );
unset( $qv['my_multitype'] );
}
return $qv;
} );Kuncinya ada di langkah 3: dengan $qv['post_type'] = array( 'post', 'article' ), WordPress mencari slug name di kedua tipe konten dalam satu query. Inilah "fallback" yang sebenarnya kita inginkan, tanpa perlu dua rewrite rule. Penanda my_multitype di-unset supaya tidak bocor ke query final.
Auto-flush rewrite rule setelah deploy
Rewrite rule baru tidak aktif sampai di-flush. Daripada manual menyimpan permalink settings, saya pasang auto-flush yang dipatok ke versi, dijalankan di init setelah rule didaftarkan:
add_action( 'init', function () {
$version = '1.0.1'; // naikkan setiap kali rule berubah
if ( get_option( 'my_rewrite_version' ) !== $version ) {
flush_rewrite_rules();
update_option( 'my_rewrite_version', $version );
}
}, 99 ); // prioritas akhir, setelah add_rewrite_rule terdaftarflush_rewrite_rules() itu mahal, jadi jangan dipanggil setiap request. Pola version-pin ini hanya flush sekali per deploy, saat versinya naik, lalu menyimpannya di wp_options.
Catatan penutup
- Rewrite rule di-key oleh regex. Mendaftarkan regex yang sama dua kali akan menimpa, bukan chaining. Hanya satu yang hidup.
- Jangan harap fallthrough antar dua rule beregex sama. WordPress tidak punya mekanisme itu.
- Pakai satu rule + private query var, lalu mekarkan di filter
request. Untuk multi-tipe, setpost_typejadi array. Itu fallback yang sesungguhnya. - Jangan lupa
unsetpenanda query var. Kalau dibiarkan, ia bisa mengacaukan query final. - Auto-flush yang dipatok versi.
flush_rewrite_rules()mahal; jalankan sekali per deploy lewat opsi versi, jangan tiap request.
Pelajaran intinya: rewrite rule itu sebuah map yang di-key oleh regex, bukan daftar berurut yang dicoba satu per satu. Begitu Anda memikirkannya sebagai map, solusinya jelas: satu key, satu rule, dan biarkan filter request yang mengurus percabangan.