"Kenapa? Biasanya auto resize kok."
Itu tanggapan saya waktu editor di pihak klien melaporkan, pada 1 September, bahwa gambar hero di satu halaman tidak lagi mengisi bingkainya. Ada celah di sepanjang sisi kanan dan sepanjang sisi bawah. Heran saya bukan tanpa dasar, karena sebelumnya gambar di tema itu memang selalu pas dengan bingkainya. Markup temanya juga benar, dan tidak ada yang berubah di sana.
Templatenya sama, hasilnya berbeda, dan jawabannya ternyata ada di markup yang sampai ke peramban, bukan di template.
Gambar 768 di bingkai 880
Hasil ukur di hero-nya: bingkai 880x495, gambar 768x432, dan gambarnya menempel di kiri atas. Kalau dibagi, 768 per 880 sama dengan 0,8727, dan 432 per 495 juga 0,8727, jadi gambarnya mengisi 87 persen di kedua sumbu. Dua angka itu bisa kembar karena bingkai dan gambarnya punya rasio yang sama. Bingkainya diberi aspect-ratio: 16 / 9, dan 768x432 juga tepat 16:9.
Gambarnya sama sekali tidak meregang mengikuti bingkai. Dia tergambar pada ukurannya sendiri di kiri atas, dan sisa ruangnya menjadi celah kanan dan bawah yang dilaporkan tadi. Di catatan saya, pola 87/87 ini saya tulis sebagai tanda khas masalahnya: kalau melihat pola itu, cari elemen pembungkus.
Class-nya pindah ke <picture>
Pembungkus itu datang dari Imagify. Imagify 2.3.1 di situs itu berjalan dengan display_nextgen bernilai 1 dan display_nextgen_method bernilai picture, dan nilai picture itulah penyebabnya. Mode ini menulis ulang setiap <img class="..."> menjadi <picture class="..."><img></picture>, lalu memindahkan atribut class ke pembungkusnya.
Bingkai di tema ini meletakkan w-full h-full object-cover di elemen img. Setelah penulisan ulang, bentuknya kurang lebih begini:
<!-- Disederhanakan. Yang ditulis template: -->
<span class="block overflow-hidden" style="aspect-ratio: 16 / 9;">
<img src="..." alt="" class="w-full h-full object-cover">
</span>
<!-- Disederhanakan. Yang ada di DOM setelah Imagify dan lazyload WP Rocket: -->
<span class="block overflow-hidden" style="aspect-ratio: 16 / 9;">
<picture class="w-full h-full object-cover">
<img src="..." alt="" class="entered lazyloaded">
</picture>
</span><picture> berstatus display: inline, jadi width: 100% dan height: 100% tidak berpengaruh apa-apa. object-fit sama sekali tidak bekerja di sana, karena <picture> bukan replaced element. Sementara <img> di dalamnya, elemen yang sebenarnya menggambar foto, hanya kebagian class entered lazyloaded dari lazyload WP Rocket. Ketiga class ukuran tadi masih ada di halaman, hanya saja menempel di elemen yang tidak bisa memakainya.
Saya pernah menulis soal <picture> yang inline dan object-fit yang salah alamat. Bedanya, waktu itu <picture>-nya saya tulis sendiri di template, dan perbaikannya cukup dengan membuang elemen itu. Di sini template hanya mengeluarkan <img> biasa di dalam bingkai. Plugin-lah yang menulis ulang markup itu setelah template selesai mengerjakannya, jadi tidak ada <picture> di template yang bisa dibuang.
Kenapa baru rusak sekarang
Penulisan ulang ke <picture> butuh WebP yang sudah ada. Karena itu masalahnya baru menggigit setelah gambar tertentu itu punya versi WebP, sementara setiap unggahan yang lebih lama masih disajikan sebagai <img> polos dan tampil sempurna. Unggahan editor tadi di bulan September adalah gambar pertama yang sudah dikonversi Imagify, jadi gambar itu pula yang pertama rusak. Ini yang membuat tanggapan saya tadi masuk akal: sampai unggahan itu, semua gambar di tema ini masih berupa <img> polos.
Akibatnya, ini bukan bug satu halaman. Tema ini punya 24 template yang memakai bingkai rasio tetap plus object-cover, dan masing-masing akan rusak pada jadwalnya sendiri begitu gambarnya dikonversi. Setiap bingkai seperti itu rusak diam-diam, dan hanya setelah gambarnya punya WebP. Di tema ini, satu halaman yang rusak bukan berarti satu bug.
Lima aturan di luar layer
Perbaikan yang saya pasang berupa lima aturan di akhir assets/css/input.css:
picture.w-full { display: block; width: 100%; }
picture.h-full { display: block; height: 100%; }
picture.object-cover > img,
picture.object-contain > img { display: block; width: 100%; height: 100%; }
picture.object-cover > img { object-fit: cover; }
picture.object-contain > img { object-fit: contain; }Aturan-aturan ini sengaja tidak dimasukkan ke layer mana pun, supaya mengungguli layer utility. Tailwind v4 menaruh utility-nya di dalam cascade layer, dan deklarasi biasa yang berada di luar layer selalu menang atas deklarasi di dalam layer, berapa pun specificity-nya. Selector-nya juga dibatasi pada pembungkus yang benar-benar membawa class tersebut, sehingga gambar dari editor di dalam isi post tidak ikut tersentuh.
Ada alternatif sekali klik yang tidak saya ambil, yaitu mengganti Imagify ke display_nextgen_method = rewrite. Cara itu juga akan berhasil, karena servernya Apache dengan .htaccess yang bisa ditulis. Saya memilih CSS karena CSS itu tinggal di repo sendiri, tetap bertahan kalau ada yang menyetel ulang plugin, dan ikut menutup plugin lain yang melakukan penulisan ulang yang sama. Komentar di atas blok aturan itu mencatat kapan dia boleh dihapus, yaitu kalau Imagify pindah ke metode rewrite.
Build ulang yang dicek lewat selector
npm run build di tema menjalankan CLI Tailwind dari input.css ke tailwind-compiled.css dengan --minify, dan versi Tailwind-nya dipatok di 4.2.4. node_modules tema tidak selamat waktu saya pindah ke Mac, jadi npm install harus jalan dulu. Install itu ikut membetulkan package-lock.json, yang ternyata mencatat ^4.1.14 padahal package.json mematok persis 4.2.4. Itu sebabnya commit perbaikannya menyentuh tiga berkas, bukan dua: input.css, tailwind-compiled.css, dan package-lock.json.
Hasil build ulangnya saya verifikasi dengan membandingkan himpunan selector, bukan byte. Hasilnya 551 selector sebelum, 552 sesudah, dan tidak satu pun yang hilang. Satu yang bertambah adalah .gap-y-5, yang berarti artefak yang sudah di-commit sebelumnya sedikit basi terhadap markup.
Dari mana angka 768 itu
Commit perbaikan Imagify langsung disusul satu commit lain dengan cap waktu yang sama sampai ke detik, khusus untuk ukuran hero. Pesannya mencatat bahwa petunjuk sizes hero waktu itu mengklaim (min-width: 1024px) 48rem, padahal hero duduk di dalam pembungkus yang mentok di 880px. Lebar yang dideklarasikan kurang 112px itu membuat peramban memilih kandidat 768w untuk bingkai 880px, sehingga gambar tampil agak lembut bahkan setelah mengisi bingkai dengan benar.
Angka-angka ini menjelaskan 768 di pengukuran awal. Lebar yang dideklarasikan adalah 880 dikurangi 112, yaitu 768px, dan kandidat yang dipilih peramban juga 768w. Gambar yang tergambar pada ukurannya sendiri di bingkai 880 pun selebar 768, persis seperti hasil ukur, dan 768 dibagi 880 itulah 87 persen tadi.
Atribut sizes hero kini berbunyi (min-width: 936px) 880px, 94vw. Komentar di template menjelaskan angka 936: di titik itulah container berhenti menjadi pembatas, dan 880 yang mengambil alih. Hitungannya cocok, karena 94 persen dari 936 adalah 879,84, praktis 880.
Dua jebakan lama yang terulang
Selama pengerjaan, saya juga masuk ke dua jebakan yang sebenarnya sudah tertulis di catatan saya sendiri.
Yang pertama ada di curl. Saya menulis sendiri -H 'Accept-Encoding: zstd, gzip, deflate, br' di samping --compressed. Akibatnya curl menawarkan zstd yang tidak bisa dia dekode, body-nya tidak pernah ditulis, dan gejalanya -D menghasilkan header sementara -o tidak menghasilkan berkas sama sekali. Pelajarannya tetap sama dengan yang sudah tertulis di catatan lama: pakai --compressed saja.
Yang kedua ada di edge. Deploy-nya benar, dan origin sudah memuat perbaikan, baik di berkas tema mentah maupun di salinan min milik WP Rocket, tapi edge-nya basi seluruhnya. Halaman publik masih HIT dengan sizes lama, dan menunjuk token ?ver= lama yang CSS-nya tidak memuat satu pun aturan baru. Satu purge_everything yang saya jalankan membereskan keduanya, karena HTML basi itulah yang mengunci token lama. Selama HTML lama yang dilayani, peramban akan terus meminta CSS lama.
Mengukur, bukan mengira-ngira
Untuk perbaikan jenis ini, cara memverifikasinya adalah mengukur: muat halamannya, bandingkan kotak bingkai dengan kotak gambar, lalu baca getComputedStyle(img).objectFit. Satu hal lagi wajib dipastikan, yaitu tidak ada stylesheet uji yang masih terpasang, lewat document.getElementById(...). Tanpa itu, yang terbukti bisa saja suntikan saya sendiri, bukan CSS yang ter-deploy. Contoh minimalnya, dengan selector dan id yang netral:
// Contoh minimal. Selector dan id-nya netral, sesuaikan dengan halamanmu.
const frame = document.querySelector('.frame');
const img = frame.querySelector('img');
const f = frame.getBoundingClientRect();
const i = img.getBoundingClientRect();
console.log(i.width / f.width, i.height / f.height); // rasio isi di dua sumbu
console.log(getComputedStyle(img).objectFit);
console.log(document.getElementById('stylesheet-uji') === null); // harus trueCatatan saya hanya menyimpan metodenya. Angka ukur bingkai dan gambar sesudah perbaikan tidak ikut tercatat, jadi tidak saya tulis di sini. Kalimat yang paling dekat dengan hasil visualnya justru ada di pesan commit sizes tadi, yang menyebut gambar masih agak lembut bahkan setelah mengisi bingkai dengan benar.
Judul catatan saya untuk kasus ini merangkumnya: plugin bisa menulis ulang markup setelah template selesai mengerjakannya. Template yang benar belum menjamin DOM yang benar, dan di tema dengan 24 template berpola sama, laporan tentang satu halaman ternyata menyangkut semuanya.