Migrasi domain di sebuah situs editorial yang saya pindahkan ke WordPress kelihatannya berjalan mulus. Saya jalankan Better Search Replace, tukar https://old-site.com/ jadi domain baru, klik "replace all", dan plugin melaporkan ribuan perubahan. Selesai, pikir saya. Lalu saya klik-klik beberapa halaman dan setengah link internal masih mengarah ke domain lama. Bukan link acak — pola yang jelas: link di dalam paragraf biasa berhasil, tapi link di dalam blok-blok Gutenberg tertentu tidak. Tombol, galeri, blok dengan atribut — semua masih menunjuk old-site.com.
Yang bikin saya hampir tertipu: Better Search Replace bilang sukses. Tidak ada error, tidak ada baris merah. Search-replace-nya benar-benar berjalan. Masalahnya, dia tidak menemukan apa yang saya kira sudah dia temukan.
Trigger-nya kira-kira begini. Inilah string yang saya cari, dan inilah bentuk yang sebenarnya tersimpan di database untuk konten Gutenberg:
Yang saya cari:
https://old-site.com/
Yang tersimpan di markup blok Gutenberg:
<!-- wp:button -->
... "url":"https:\/\/old-site.com\/contact" ...
<!-- /wp:button -->Lihat slash-nya. Di paragraf biasa URL-nya literal: https://old-site.com/. Tapi di dalam atribut blok, setiap forward slash di-escape jadi \/, sehingga URL yang sama disimpan sebagai https:\/\/old-site.com\/. Pencarian saya untuk bentuk polos tidak akan pernah cocok dengan bentuk yang di-escape itu.
Kenapa ini terjadi
Gutenberg menyimpan atribut blok sebagai JSON yang ditempelkan ke dalam komentar HTML di post_content. Dan di JSON, forward slash boleh — secara opsional — di-escape jadi \/. WordPress (lewat wp_json_encode / json_encode PHP) memang melakukan ini secara default. Jadi URL yang Anda tulis di editor sebagai https://old-site.com/contact berakhir di database sebagai https:\/\/old-site.com\/contact begitu masuk ke atribut blok seperti wp:button, wp:image, atau apa pun yang menyimpan href di JSON-nya.
Better Search Replace itu pencocokan string mentah terhadap isi kolom database. Dia tidak mem-parse JSON, tidak meng-unescape apa pun, tidak "paham" bahwa https:\/\/old-site.com dan https://old-site.com adalah URL yang sama. Bagi dia itu dua string berbeda. Jadi ketika saya hanya mencari bentuk polos, dia dengan patuh mengganti setiap kemunculan bentuk polos — paragraf, link klasik, custom field yang tidak diserialisasi — dan diam-diam melewati setiap URL yang slash-nya di-escape di dalam blok. Setengah situs ter-update, setengahnya tidak, dan laporan "berhasil" tetap hijau karena memang teknisnya berhasil untuk apa yang dia cocokkan.
Ini bukan kuirk khusus Gutenberg saja. Data ter-serialisasi atau JSON apa pun — atribut blok, data widget di wp_options, array meta ter-serialisasi — menyimpan slash sebagai \/. Pola yang sama akan menggigit Anda di mana pun.
Perbaikannya
Jalankan operasi search-replace dua kali: sekali untuk bentuk polos, sekali untuk bentuk yang di-escape. Konkretnya, dua pasang ini:
# Pass 1 — bentuk polos (paragraf, link klasik, dll.)
Cari: https://old-site.com
Ganti: https://new-site.com
# Pass 2 — bentuk ter-escape (atribut blok Gutenberg, data widget, JSON)
Cari: https:\/\/old-site.com
Ganti: https:\/\/new-site.comPass kedua itu yang menyentuh blok Gutenberg. Anda mencari slash yang di-escape secara harfiah dan menggantinya dengan slash yang di-escape juga, jadi JSON tetap valid setelahnya — Anda tidak pernah mencampur kedua bentuk di dalam satu atribut.
Satu jebakan saat domain Anda rawan tabrakan — misalnya satu URL adalah substring dari yang lebih panjang, atau Anda punya old-site.com dan blog.old-site.com. Pencocokan string yang naif akan merusak URL yang lebih panjang. Jangkar pencariannya. Saya tambahkan tanda kutip penutup atau pakai konteks href="..." penuh supaya hanya kecocokan yang tepat yang kena:
# Diberi jangkar supaya tidak menyentuh sub-path atau subdomain yang lebih panjang
Cari: href="https://old-site.com"
Ganti: href="https://new-site.com"Dua hal lagi yang menyelamatkan saya dari menebak-nebak. Pertama, selalu jalankan dry run dulu. Better Search Replace punya mode "dry run" yang menghitung tanpa menulis ke database — saya pakai itu di setiap pass sebelum melepasnya beneran. Kedua, perhatikan apa yang dilaporkan plugin: dia melaporkan jumlah PERUBAHAN, bukan jumlah kecocokan. Jadi cara mengonfirmasi kelengkapan adalah dengan menjalankan ulang pass yang sama sampai dia melaporkan 0 perubahan. Nol perubahan berarti tidak ada lagi yang tersisa dari bentuk itu. Selama angkanya bukan nol, masih ada yang ketinggalan.
Setelah pass ter-escape, saya jalankan ulang, dapat angka besar, jalankan lagi sampai nol, lalu cek halaman-halaman yang tadinya bermasalah. Link tombol dan galeri akhirnya menunjuk domain baru.
Pelajaran
Migrasi URL bukan satu operasi find-replace — minimal dua, karena konten ter-serialisasi dan JSON (blok Gutenberg, data widget, meta ter-serialisasi) menyimpan slash sebagai \/. Kalau Anda hanya mengganti bentuk polos, Anda meninggalkan setengah link Anda secara diam-diam, dan laporan "sukses" akan menutupinya. Selalu jalankan bentuk mentah dan bentuk \/ yang di-escape, jangkar pencarian yang rawan tabrakan dengan konteks href="...", mulai dengan dry run, dan jalankan ulang sampai plugin melaporkan nol perubahan — karena yang dia hitung itu perubahan, bukan kecocokan. Hijau bukan berarti selesai; nol berarti selesai.
