Impor kontennya selesai tanpa satu pun error dan tiap jumlah baris yang dicetak cocok dengan yang saya harapkan. Baru waktu saya buka situsnya di browser kelihatan bahwa tiap posisi gambar isinya kotak rusak, di mana-mana. Markupnya jujur soal sebabnya:
<img src="MEDIA.cover">Itu bukan URL. Itu nama variabel yang tersimpan sebagai teks.
Yang bikin mahal, bug ini baru muncul setelah deploy-nya selesai dengan sukses. Angkanya wajar, tidak ada yang gagal, jadi tidak ada satu pun sinyal untuk berhenti lebih awal. Ongkosnya satu siklus penuh yang terbuang: bersihkan data sisa, semai ulang, impor penuh, baru ketahuan.
Tiga berkas, dan yang ketiga yang mem-parse
Deploy tema untuk situs klien ini jalan lewat tiga berkas yang dipanggil berurutan. Yang pertama menghapus data sisa. Yang kedua menyemai delapan penulis, seluruh term taksonomi, sembilan belas halaman statis bersarang lengkap dengan resolusi post_parent, dan opsi situsnya. Yang ketiga mem-parse berkas konten TypeScript pakai regex PHP dengan pejalan kurung buatan sendiri yang paham string dan komentar, lalu menyemai lima custom post type.
Temanya sendiri tema WordPress kustom hasil port dari sumber React 19 plus Vite 7 plus Tailwind v4, dan ketujuh belas templatenya saya port dalam satu sesi.
Pejalan kurungnya bagian yang bekerja. Yang tidak bekerja pembaca nilainya.
Konvensinya menaruh aset di puncak berkas
Berkas konten dari generator sumber React itu punya kebiasaan meng-hoist aset bersama ke satu objek di puncak berkas, lalu array isinya merujuk ke sana lewat identifier.
export const MEDIA = {
cover: "https://cdn.example.net/cover.webp",
portrait: "https://cdn.example.net/portrait.webp",
// ...
};
export const FEATURES = [
{ slug: "profile-one", img: MEDIA.cover, /* ... */ },
{ slug: "profile-two", img: MEDIA.portrait, /* ... */ },
];Buat TypeScript ini biasa saja. Buat parser regex, MEDIA.cover cuma deretan karakter yang kebetulan tidak diapit tanda kutip.
Pembaca nilai yang naif mengembalikan tokennya apa adanya
Pembaca nilai di importer punya cabang untuk identifier, dan cara kerjanya sesederhana ini: baca sampai ketemu koma, kurung kurawal, atau baris baru, lalu kembalikan tokennya. Untuk img: MEDIA.cover yang dia kembalikan adalah string "MEDIA.cover", bukan URL CDN yang ditunjuk key itu.
String tadi lalu tersimpan sebagai post meta gambar eksternal. Situs ini memang menyimpan gambar CDN eksternal di post meta dan membacanya lewat satu helper yang mendahulukan featured image WordPress dan jatuh ke URL eksternal kalau featured image-nya tidak ada. Helper itu bekerja persis seperti yang saya tulis: tidak ada featured image, jadi dia pakai nilai eksternalnya, dan nilai eksternalnya kebetulan bukan URL. Yang sampai ke front-end adalah tag img dengan src yang tidak bisa diapa-apakan browser.
Tidak ada yang error di sepanjang rantai itu. Parser mengembalikan string, update_post_meta menyimpan string, helper mengembalikan string, template mencetak string. Tiap komponennya sukses, dan hasil gabungannya rusak total.
Perbaikannya: bangun mapnya sebelum seeder jalan
Aturan yang saya tarik dari kejadian ini cuma satu kalimat: importer wajib me-resolve rujukan identifier simbolik yang menunjuk ke registry di puncak berkas, bukan menyimpan teks tokennya.
Urutannya yang penting. Sebelum seeder array konten jalan, tiap registry di puncak berkas di-parse sekali jadi map key ke nilai. Perbaikan yang saya pasang menambah empat hal: ekstraktor badan objek yang mencerminkan ekstraktor array yang sudah ada, tapi untuk bentuk {...}; pembangun map yang menarik entri registry jadi map key ke URL; satu global yang diisi di awal run; dan resolusi di cabang identifier pembaca nilai.
global $theme_media_map;
$theme_media_map = theme_build_media_map( $src ); // key => URL, sekali di awal run
// Di dalam cabang identifier theme_read_value():
if ( preg_match( '/^MEDIA\.(\w+)$/', $token, $m ) ) {
global $theme_media_map;
if ( isset( $theme_media_map[ $m[1] ] ) ) {
return $theme_media_map[ $m[1] ];
}
}
return $token;Yang menentukan letaknya: pencocokan ^MEDIA\.(\w+)$ harus terjadi di cabang identifier sebelum token dikembalikan, bukan sesudahnya di lapisan penyimpanan. Begitu token literal lolos dari pembaca nilai, tidak ada lagi yang tahu bahwa itu seharusnya rujukan.
Cetak ukuran mapnya di awal run
Satu baris verifikasi yang murah: cetak ukuran map dan daftar key-nya di awal tiap run impor.
echo 'MEDIA map keys: ' . count( $theme_media_map )
. ' (' . implode( ', ', array_keys( $theme_media_map ) ) . ')';Di run yang sehat baris itu mencetak MEDIA map keys: 12. Nol atau sebagian berarti regexnya melewatkan entri, dan itu harus dibereskan sebelum lanjut. Persis ini yang absen di run pertama. Satu-satunya angka yang tercetak waktu itu adalah jumlah post per tipe, dan jumlah post memang benar, jadi angkanya malah menenangkan.
Idempoten di level meta, bukan cuma di level post
Impornya melewati post yang slug-nya sudah ada. Kalau update meta ikut digantung di balik pemeriksaan "baru disisipkan", memperbaiki parser tidak akan menyembuhkan apa pun, karena post-nya sudah ada semua dan tidak ada satu pun yang disentuh lagi. Jadi importernya harus idempoten di level meta: insert post boleh skip-by-slug, tapi update_post_meta jalan tanpa syarat.
$post_id = theme_find_by_slug( $slug );
if ( ! $post_id ) {
$post_id = wp_insert_post( $args );
}
// Selalu jalan, bukan cuma untuk post yang baru dibuat.
update_post_meta( $post_id, '_theme_external_image', $image_url );Karena itu, menjalankan ulang impor memperbaiki semua post yang sudah ada. Perbaikannya saya terapkan di sesi yang sama, impornya saya ulang, dan gambarnya muncul.
Pencegahannya satu grep sebelum impor pertama
Rujukan identifier ke ekspor tetangga bukan cuma soal gambar. Registry di puncak berkas bisa dinamai apa saja, dan mana pun yang dirujuk secara simbolik wajib di-resolve importer, bukan disimpan sebagai teks token. Jadi sebelum impor pertama, grep dulu sumbernya untuk pola rujukan identifier ke ekspor tetangga.
grep -nE ': (MEDIA|WRITERS|GLYPHS)\.' data.tsUntuk tiap pola yang kena, pastikan parser punya resolvernya. Kalau belum, grep importernya untuk pola yang sama sebagai jaring pengaman.
Satu catatan supaya perbaikannya tidak kebablasan: merujuk ke registry itu bukan kesalahan sumbernya, itu memang konvensinya, dan salah satu template bahkan sengaja memakai URL CDN dari salah satu key registry untuk avatar. Resolvernya harus mengikuti key yang benar-benar tertulis di sumber, bukan menebak key dari slug barisnya.
Bug ini tidak mahal karena sulit. Dia mahal karena sabar menunggu sampai semua yang bisa saya periksa sudah terperiksa dan semuanya lulus. Sekarang, tiap kali importer saya menyentuh berkas konten TypeScript, hal pertama yang dia cetak adalah ukuran map registrinya, sebelum satu post pun dibuat.