Di sebuah situs booking properti sewa yang saya kerjakan, hero versi mobile sengaja saya bikin semurah mungkin: satu gambar diam yang dipasang sebagai background-image lewat CSS, tanpa JavaScript sama sekali. Versi desktopnya jauh lebih berat dan seluruh gambarnya diatur dari JavaScript, tapi itu cabang kode yang lain. Cabang mobile isinya satu aturan CSS. Sederhana, murah, dan seharusnya tidak bisa salah.
Lalu klien kirim screenshot dari HP-nya: hero-nya kosong. Bukan pecah, bukan salah crop. Kosong.
Gejalanya: hanya mobile, hanya di produksi
Di desktop semuanya normal. Di lokal, mobile juga normal. Yang bermasalah cuma kombinasi mobile plus build produksi. Waktu saya colok HP ke remote debugging dan buka Network tab, penyebabnya sudah menunggu di sana dengan sangat jelas:
GET /assets/css/dist/imagesequences-webp/bgimage_0001.webp
404 Not FoundSaya baca dua kali. Path itu salah, tapi salahnya dengan cara yang aneh. File gambarnya ada di assets/imagesequences-webp/, dan saya tidak pernah menulis dist di mana pun dekat URL gambar. Yang saya tulis di assets/css/hero.css adalah ini:
.hero-bg {
background-image: url("../imagesequences-webp/bgimage_0001.webp");
}Dari posisi assets/css/, ../ naik satu tingkat ke assets/, lalu masuk ke folder gambar. Benar. Saya cek tiga kali sambil mulai merasa bodoh, karena file CSS-nya jelas menuliskan yang benar.
Refleks cache saya sempat menyala sebentar lalu padam sendiri, karena string yang salah itu bukan versi lama dari apa pun yang pernah saya tulis. Perhatikan bentuknya: dist duduk di tengah URL gambar, padahal dist cuma pernah ada sebagai nama folder output build. Tidak ada tangan yang mengetik kombinasi itu. Artinya path tersebut dirakit, dan yang bisa merakitnya cuma dua pihak: browser waktu me-resolve URL relatif, atau build waktu menulis file CSS-nya. Keduanya kebetulan ikut andil.
Akar masalahnya: minifier menulis ulang URL-nya
Saya mulai dari pihak kedua, karena itu yang paling gampang dibuktikan: buka file CSS yang benar-benar dilayani produksi, bukan sumbernya. Isinya begini:
.hero-bg{background-image:url(imagesequences-webp/bgimage_0001.webp)}../-nya hilang. Bukan salah ketik saya, bukan salah salin ke server. Prefix itu dibuang oleh cssnano di pipeline PostCSS, sebagai bagian dari normalisasi URL saat minify.
Dan di sinilah dua masalah bertumpuk jadi satu 404.
Yang pertama, dan ini mekanik CSS paling dasar yang paling sering dilupakan: url() di dalam CSS resolve relatif terhadap lokasi file CSS-nya, bukan terhadap URL halaman HTML. Selama file yang dilayani adalah assets/css/hero.css, ../ menunjuk ke tempat yang benar. Tapi build saya tidak melayani file itu. Build saya menghasilkan assets/css/dist/hero.min.css, satu folder lebih dalam. Basis perhitungannya sudah geser sebelum minifier menyentuh apa pun.
Yang kedua, cssnano lalu membuang ../-nya. Jadi alih-alih path yang salah satu tingkat, saya dapat path yang menempel langsung ke folder output. assets/css/dist/ ditambah imagesequences-webp/bgimage_0001.webp, dan itulah persis string yang muncul di Network tab.
Ini sekaligus menjelaskan kenapa breakpoint-nya ikut menentukan. Cuma cabang mobile yang menyerahkan URL gambar ke CSS. Cabang desktop menerima base path frame-nya dari JavaScript, dan JavaScript tidak pernah lewat minifier CSS. Satu-satunya jalur yang memberi build kesempatan menulis ulang URL adalah jalur yang cuma hidup di layar kecil.
Perbaikannya: keluarkan URL-nya dari CSS
Opsi paling cepat jelas ada. Saya bisa mematikan normalisasi URL di preset cssnano, atau menulis path absolut dari root supaya kebal terhadap perpindahan folder. Keduanya berhasil.
Tapi keduanya punya harganya sendiri. Mematikan normalisasi URL berarti hero saya bergantung pada satu opsi di config yang sama sekali tidak kelihatan berhubungan dengan hero, dan siapa pun yang menaikkan versi dependensi nanti bisa mengembalikannya tanpa tahu apa yang dia patahkan. Path absolut dari root memang kebal terhadap perpindahan folder, tapi URL-nya tetap duduk di dalam CSS dan tetap melewati minifier setiap kali build jalan. Dua-duanya menutup 404-nya sambil membiarkan URL gambar tetap berada di tempat yang boleh ditulis ulang pihak lain.
Jadi aturan background-image-nya saya hapus sepenuhnya dari CSS. Base path frame-nya sekarang diletakkan di markup sebagai data attribute, dan JavaScript hero yang memasangnya:
<div class="hero-bg" data-frame-base="/assets/imagesequences-webp/"></div>// hero-frames.js
function initHero(isMobile) {
const hero = document.querySelector(".hero-bg");
if (!hero) return;
const base = hero.dataset.frameBase;
if (isMobile) {
hero.style.backgroundImage = `url("${base}bgimage_0001.webp")`;
return;
}
// desktop: jalankan frame sequence dari base yang sama
}Cabang mobile sekarang memakai sumber kebenaran yang sama persis dengan cabang desktop, yaitu satu base path di satu tempat. Tidak ada lagi URL gambar yang melewati PostCSS, jadi tidak ada lagi yang bisa dinormalisasi, dipendekkan, atau di-rebase oleh build.
Harganya tetap ada dan saya bayar dengan sadar. Base path-nya masih absolut dari root, jadi kalau situsnya suatu hari dipasang di subdirektori, nilainya tetap harus diubah. Bedanya, nilai itu sekarang satu atribut yang kelihatan di markup, bukan string yang terkubur di dalam CSS hasil minify. Dan karena background mobile dipasang dari JavaScript, hero itu jadi bergantung pada script yang benar-benar jalan. Kalau script-nya gagal, saya balik ke hero kosong, persis gejala yang tadi dilaporkan klien.
Cara cepat memastikan kamu kena hal yang sama
Kalau ada aset yang 404 dengan path yang mirip tapi tidak persis seperti yang kamu tulis, jangan lama-lama menatap file sumber. Baca outputnya:
grep -o 'url([^)]*)' assets/css/dist/hero.min.cssBandingkan hasilnya dengan yang ada di file sumber. Kalau berbeda, urusannya ada di build, bukan di CSS kamu. Dan sebelum menyalahkan minifier, cek dulu apakah folder output berada di kedalaman yang sama dengan folder sumber. Sering kali itu saja sudah cukup untuk mematahkan setiap path relatif di dalam file.
Yang saya bawa pulang
url()di CSS dihitung dari lokasi file CSS-nya. Begitu build memindahkan file itu ke folder lain, semua path relatif di dalamnya ikut bergeser.- Minifier boleh menulis ulang URL. cssnano menormalisasi path relatif, dan hasilnya bisa berbeda dari yang kamu tulis.
- Bug yang cuma muncul di satu breakpoint biasanya menunjukkan satu jalur kode yang berbeda, bukan masalah styling responsif.
- Path yang salah tapi bukan salah ketik itu petunjuk tersendiri. Kalau tidak ada manusia yang pernah mengetik string seperti itu, berarti ada yang merakitnya, dan perakitnya biasanya build.
- Aset yang path-nya rapuh lebih baik ditentukan sekali di markup atau JavaScript daripada dititipkan ke pipeline yang berhak mengubahnya.