D
P
0
← Semua artikel Read in English

SEO Teknis, Redirect & Migrasi

Impor 131 Redirect Sekaligus: Lima Jalur Otomasi Gagal, CSV Tanpa Header yang Berhasil

· · 7 menit baca
Impor 131 Redirect Sekaligus: Lima Jalur Otomasi Gagal, CSV Tanpa Header yang Berhasil

Bagian dari sebuah migrasi WordPress: 131 rule redirect harus masuk ke plugin redirect gratis yang sudah dipakai situsnya. Isinya 123 redirect dari URL blog lama menuju satu halaman hub, ditambah 8 redirect halaman spesifik. Halaman komersialnya sudah dikonfirmasi aman dan sengaja dikecualikan dari daftar itu. Stack-nya WordPress dan PHP, dan plugin redirect-nya cuma menyediakan tombol Add New untuk menambah rule satu per satu.

Sebelum satu pun rule baru masuk, ada dua pekerjaan bersih-bersih. Empat tabrakan redirect saya bereskan dulu, tiga di antaranya lunak dan satu berupa rule keras di dalam plugin yang harus dihapus. Lalu satu halaman tujuan saya buat, karena dari seluruh daftar cuma dia tujuan redirect yang belum ada.

Sisanya kelihatan seperti pekerjaan otomasi biasa. Di projek ini perubahan wp-admin dan basis data memang dikerjakan langsung di situs hidup lewat Playwright, tanpa unggah ulang tema. Saya coba lima cara berbeda untuk memasukkan 131 baris itu lewat UI, dan kelimanya tidak menambahkan satu rule pun.

Lima jalur yang tidak menambahkan satu rule pun

Yang pertama paling menggoda karena kelihatan paling murah. Halaman rule plugin ini pada dasarnya tabel input, jadi saya bikin barisnya sendiri dan menempelkannya ke DOM, lalu tekan Save.

// Nama field aslinya saya samarkan. Baris ini disuntik ke tabel, lalu Save ditekan.
const row = document.createElement('tr');
row.innerHTML = `
  <input name="rule[from][]" value="/story/">
  <input name="rule[to][]"   value="/insights/">
`;
table.appendChild(row);

Save berjalan tanpa error dan barisnya diabaikan begitu saja.

Cara kedua: berhenti membuat baris sendiri, biar plugin yang membuatnya. Saya panggil .click() native di tombol Add New supaya kode plugin sendiri yang menyiapkan barisnya. Gagal, dan penyebabnya isTrusted. Event yang dilahirkan script menandai dirinya sebagai bukan aksi manusia, dan di jalur ini tanda itu menentukan.

Cara ketiga: lewati UI sepenuhnya dan POST form-nya langsung. Balasannya 500, karena ada satu field internal yang tidak ikut terkirim.

Cara keempat: pakai fitur unggah berkas yang memang sudah disediakan plugin. File-picker Playwright terus timeout di titik ini.

Cara kelima bukan jalur baru, tapi hambatan yang menempel di semuanya. Plugin popup marketing di situs ini memasang overlay position: fixed yang mencuri klik, sehingga input Add dan input berkas jadi rewel. Node position: fixed dengan z-index di atas 100 harus dibuang dulu sebelum interaksi apa pun bisa diandalkan.

Akar masalahnya ada di tempat Save membaca datanya

Satu kalimat menjelaskan kenapa dua percobaan pertama gagal dengan cara yang sama sekali tidak berisik: saat Save, plugin ini hanya membaca row dari registry JS internalnya sendiri. Form HTML di layar bukan sumber kebenarannya, cuma cerminannya.

Artinya baris yang saya suntik ke DOM memang ada di halaman, tapi tidak pernah terdaftar di registry itu, jadi Save tidak melihatnya. Dan yang mendaftarkan row baru ke registry adalah handler tombol Add New, yang justru tidak mau dijalankan lewat klik script. Kesimpulannya, bulk lewat UI tidak feasible di plugin ini, sebanyak apa pun akal-akalan DOM yang dipakai.

CSV tanpa header, dan radio yang menentukan

Jalur bulk yang benar-benar bekerja ada di tab Import/Export plugin, di halaman admin options-general.php?page=<plugin>&tab=import-export. Formatnya spesifik dan gampang salah:

301,/story/,/insights/
301,/team/press-old,/team/press

Tanpa baris header sama sekali, dan kolomnya berurutan Status,RequestURL,RedirectTo. Tujuannya boleh berupa URL maupun page-id, karena plugin otomatis meresolusi URL menjadi PAGEID.

Pilihan yang paling menentukan bukan di format CSV-nya, tapi di radio mode impor. Radio Skip Duplicates berperilaku sebagai append yang aman dan tidak menghapus rule yang sudah ada. Untuk situs yang tabel redirect-nya sudah berisi ratusan baris warisan, salah pilih di sini berarti membuang rule orang lain.

Berkas yang akhirnya benar-benar terunggah berisi 123 baris, dan saya unggah sendiri lewat browser saya karena file-picker Playwright tetap timeout.

699 rule, 10 duplikat, lalu 689

Verifikasinya tidak cukup dengan melihat tabel penuh. Saya ekstrak ulang seluruh rule dari plugin, saya diff dengan daftar rencananya, lalu saya spot-check status 301 di URL mentah. Hasilnya: 131 redirect yang direncanakan sudah hidup semua, 0 dari 131 hilang, dengan total 699 rule di plugin.

Diff itu juga memunculkan 10 rule dengan from yang sama. Semuanya tidak berbahaya karena sumber yang sama menunjuk ke tujuan yang sama, tapi asalnya menarik. Delapan di antaranya duplikat bawaan yang sudah ada di data produksi hasil clone, bukan buatan saya. Dua sisanya memang milik saya, akibat test-import dan full-import yang bertumpang tindih.

Yang bikin dua duplikat itu lolos adalah cara Skip Duplicates membandingkan: dia melihat tujuan yang sudah diresolusi, dan membandingkan bentuk PAGEID lawan bentuk URL. Dua entri yang secara praktis identik terbaca berbeda, jadi filter duplikatnya lewat. Ini konsekuensi langsung dari fitur auto-resolve yang tadi terdengar ramah.

Setelah 10 salinan duplikat itu dibuang, angkanya jadi 699 dikurangi 10, yaitu 689 rule dengan 0 duplikat.

Menghapus rule itu AJAX, bukan tautan GET

Membuang 10 baris tadi memunculkan temuan kecil yang menghemat waktu di kemudian hari. Tombol Delete per-row di plugin ini bukan tautan biasa. Dia POST AJAX ke /wp-admin/admin-ajax.php.

// Nama action, nama global, dan parameter aslinya saya samarkan.
await fetch('/wp-admin/admin-ajax.php', {
  method: 'POST',
  credentials: 'same-origin',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    action: 'rdr_delete_entry',
    _ajax_nonce: window.rdr_cfg.nonce_delete,
    id: String(ruleId),
  }),
});

Yang penting dicatat: href GET bergaya ?remove_rule=ID yang terlihat di markup tidak menghapus apa pun kalau dipanggil sendirian, karena nonce AJAX-nya tetap dibutuhkan. Kalau Anda pernah menghapus rule dengan cara membuka URL itu satu per satu lalu heran kenapa tabelnya tidak berubah, di sinilah jawabannya. Semua nonce plugin ini duduk di satu global JS, jadi begitu Anda tahu nama globalnya, seluruh operasi AJAX-nya bisa dipakai ulang.

384 baris yang justru tidak boleh diimpor

CSV redirect yang datang bersama migrasi ini sebenarnya jauh lebih panjang dari 131 baris. Ada 384 baris lain yang memetakan URL episode lama /episode/{slug}/ ke satu halaman hub, dan baris-baris itu sengaja tidak dimasukkan ke plugin.

Penanganannya dipindah ke rewrite yang bekerja dinamis: URL episode lama /episode/{slug}/ sekarang 301 ke URL barunya /series/{show}/{slug}/. Bedanya besar. Cara ini menjaga SEO per episode, sementara CSV-nya secara harfiah meminta seluruh 384 URL itu ditumpuk ke satu hub, yang akan mematikan 384 episode yang masih hidup.

Pelajaran yang saya pegang dari sini: berkas redirect hasil migrasi adalah usulan, bukan perintah. Baris yang jumlahnya ratusan dan berpola sama justru yang paling layak dicurigai sebelum di-bulk-apply.

Satu URL yang tetap membalas 200

Spot-check menemukan satu URL yang membandel. /guides/ membalas 200, bukan 301, padahal rule-nya sudah ada. Dua hal menutupi rule itu sekaligus: masih ada Page berjudul serupa yang statusnya terbit, dan cache halaman di origin ikut menaungi respons lamanya.

Ada dua opsi di sini, dan salah satunya bukan keputusan saya. Rule-nya bisa dibuang kalau pihak klien memang ingin mempertahankan halaman itu, dan itu keputusan di sisi mereka. Yang dipilih adalah opsi sebaliknya, yaitu tetap meredirect. Page root-nya saya trash lewat REST, bukan hapus permanen, supaya bisa dikembalikan kalau ternyata salah:

// Tanpa force, WordPress memindahkan Page ke trash dan bukan menghapusnya permanen.
await fetch(`/wp-json/wp/v2/pages/${pageId}`, {
  method: 'DELETE',
  credentials: 'same-origin',
  headers: { 'X-WP-Nonce': window.wpApiSettings.nonce },
});

Sisi WordPress-nya selesai, rule redirect-nya ada, dan /guides/ tetap 200. Kali ini penyebabnya bukan lagi WordPress:

# /guides/ diambil lewat CDN, bukan langsung ke origin
HTTP/2 200
<cdn>-cache-status: HIT
age: 1238
cache-control: max-age=3600

CDN di depan situs menyimpan halaman itu di cache-nya. Yang bikin ini penting jauh melampaui satu URL: migrasi ini berjalan dengan asumsi CDN-nya menyimpan HTML selama 1 detik, padahal max-age=3600 berarti 1 jam. Setiap perubahan redirect atau konten bisa tertunda sampai satu jam di URL yang sudah tercache.

Angka age di atas membuat penundaannya konkret. Salinan itu sudah berumur 1238 detik, sekitar 20,6 menit kalau dibagi 60, dan batasnya 3600 detik. Sisanya 3600 dikurangi 1238, yaitu 2362 detik atau sekitar 39,4 menit sebelum salinan itu kedaluwarsa sendiri. Redirect-nya akan mulai bekerja saat cache CDN habis dalam waktu kurang dari satu jam, atau lebih cepat kalau ada yang melakukan purge manual untuk URL itu.

Pelajaran