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.
