D
P
0

WordPress & PHP

Custom Post Type di URL Root (`/judul/`) Malah Jatuh ke Homepage — Perbaikannya dengan Rewrite Rule per-Slug

24 Juni 2026·4 menit baca
Custom Post Type di URL Root (`/judul/`) Malah Jatuh ke Homepage — Perbaikannya dengan Rewrite Rule per-Slug

Saya pernah membangun sebuah situs multi-listing di WordPress dengan satu permintaan yang kelihatannya sepele: setiap entri custom post type harus tinggal di URL root, persis /judul-entri/, tanpa prefix /book/ sama sekali. Tidak ada pesan error merah, tidak ada warning di log, tidak ada notice PHP. Yang terjadi justru lebih membingungkan: untuk sebagian besar slug, halaman yang muncul adalah HOMEPAGE, bukan single post dari CPT-nya.

Gejalanya bisa direproduksi dengan gampang. Saya buka /buku-pertama/ di browser, dan alih-alih konten single, yang dirender adalah front page. Cek cepat di template lewat conditional tag pun mengonfirmasi kecurigaan saya:

add_action('wp', function () {
    error_log('is_home: ' . var_export(is_home(), true));
    error_log('is_singular(book): ' . var_export(is_singular('book'), true));
});
// Output untuk /buku-pertama/ :
// is_home: true
// is_singular(book): false

WordPress menganggap /buku-pertama/ sebagai query homepage. Padahal post-nya jelas ada, slug-nya benar, dan kalau saya akses lewat /?post_type=book&name=buku-pertama semuanya tampil normal. Jadi datanya tidak hilang — yang salah adalah cara URL bersih itu dipetakan ke query.

Pendekatan awal saya adalah yang paling sering dianjurkan di internet: hook ke filter request lalu utak-atik query vars secara manual.

add_filter('request', function ($query_vars) {
    if (!empty($query_vars['name']) && empty($query_vars['post_type'])) {
        $query_vars['post_type'] = 'book';
    }
    return $query_vars;
});

Di atas kertas masuk akal. Pada praktiknya, untuk banyak slug, ini diam-diam jatuh ke is_home() dan homepage tetap yang dirender. Butuh waktu cukup lama sampai saya paham kenapa.

Kenapa ini terjadi

Inti masalahnya ada di urutan resolusi. Ketika WordPress menerima /judul/, ia harus menebak ini permintaan apa: front page, sebuah Page, sebuah post, atau sesuatu yang lain. Filter request itu jalan terlambat dan tidak diandalkan untuk memetakan /judul/ polos ke sebuah CPT. Pada saat filter saya menambahkan post_type=book, query vars dasarnya sudah terbentuk dengan asumsi yang salah — WP sudah keburu meresolve URL bare itu sebagai query front page / Page lebih dulu.

Jadi $query_vars['name'] yang saya andalkan sering kali memang tidak terisi untuk pola root seperti ini, atau terisi tapi dalam konteks query yang sudah condong ke homepage. Akibatnya WP_Query mengeksekusi query front page, is_home() jadi true, dan menambal post_type belakangan tidak mengubah keputusan yang sebenarnya sudah diambil di tahap rewrite. Saya bukan memperbaiki pemetaan URL — saya cuma menempel plester di atas query yang sudah salah arah.

Pelajaran kecilnya: kalau pemetaan dari URL bersih ke query var-nya sendiri yang rusak, memperbaikinya di lapisan request itu kebetulan-kebetulan, bukan deterministik. Yang benar adalah memperbaiki di lapisan rewrite, tempat URL bersih sebenarnya diterjemahkan.

Perbaikannya

Solusinya adalah mendaftarkan rewrite rule eksplisit yang menangkap pola root dan langsung memetakannya ke CPT. Tidak ada daftar slug yang di-hardcode — aturannya sepenuhnya dinamis lewat capture group:

add_action('init', function () {
    add_rewrite_rule(
        '^([^/]+)/?$',
        'index.php?post_type=book&name=$matches[1]',
        'top'
    );
});

Regex ^([^/]+)/?$ menangkap satu segmen di root (apa pun yang bukan garis miring), dan $matches[1] mengalir ke query var name dengan post_type=book sudah dipaksa di sana. Karena ditaruh di posisi top, rule ini ikut dievaluasi lebih awal dan URL bersih langsung diresolve sebagai single CPT, bukan jatuh ke front page.

Tapi rewrite rule baru tidak berlaku sampai rewrite rules di-flush. Saya tidak mau editor harus buka Settings > Permalinks setiap kali, jadi saya pasang auto-flush yang dipicu di save_post dan digerbangi sebuah option versi supaya tidak flush di setiap penyimpanan:

add_action('save_post', function () {
    if (get_option('book_rewrite_version') !== '2') {
        flush_rewrite_rules();
        update_option('book_rewrite_version', '2');
    }
});

Sekali flush jalan, gate versi mematikan dirinya sendiri — flush_rewrite_rules() itu berat, jadi tidak boleh dipanggil di setiap request. Naikkan angka versinya kalau suatu saat saya mengubah aturannya, dan ia akan flush sekali lagi secara otomatis.

Ada satu jebakan yang harus saya tangani jujur: catch-all di level root itu rakus. ^([^/]+)/?$ juga cocok dengan slug Page asli seperti /tentang/ atau /kontak/, jadi ia bisa membayangi Page sungguhan dan membuatnya tak bisa diakses. Cara saya menjaganya: beri prioritas ke Page lebih dulu — cek get_page_by_path() sebelum memperlakukan slug sebagai entri CPT, atau daftarkan rule ini di bawah penanganan Page sehingga Page menang lebih dulu.

add_action('init', function () {
    add_rewrite_rule(
        '^([^/]+)/?$',
        'index.php?post_type=book&name=$matches[1]',
        'top'
    );
}, 11); // jalan setelah handler Page bawaan
 
// Lapisan pengaman tambahan: biarkan Page asli lewat tanpa disentuh.
add_filter('request', function ($qv) {
    if (!empty($qv['name']) && get_page_by_path($qv['name'])) {
        return ['pagename' => $qv['name']];
    }
    return $qv;
});

Dengan begini, slug yang cocok dengan Page asli diserahkan ke Page, dan sisanya jatuh ke CPT seperti yang diinginkan.

Pelajaran

Untuk menaruh sebuah custom post type di root situs, filter request adalah alat yang salah — ia jalan terlalu telat dan memetakan /judul/ polos secara tidak andal, sehingga diam-diam jatuh ke homepage. Pakai rewrite rule asli yang menangkap pola root secara dinamis, pasangkan dengan auto-flush yang digerbangi option versi supaya editor tidak pernah harus menyentuh Settings > Permalinks, dan beri Page prioritas lebih dulu supaya catch-all root-mu tidak menelan halaman yang sah. Perbaiki pemetaan URL di lapisan tempat ia benar-benar terjadi, bukan menempel tambalan di hilir.