Di sebuah situs klien berbasis Shopify, saya diminta menampilkan blok review Judge.me di halaman produk (PDP). Judge.me punya dokumentasi publik untuk embed Review Widget-nya, jadi saya kira ini pekerjaan sepuluh menit: rakit sendiri div widget-nya di section Liquid, isi atribut data yang disebut di docs, tempel skrip hidrasi, selesai. Saya bangun embed manual: sebuah <div> dengan data-widget='widget', lalu saya suapi data review lewat window.judgeme_review_widget_data, persis seperti yang saya baca.
Hasilnya: tidak ada apa-apa. Div-nya ada di DOM, saya bisa lihat di inspector, tapi isinya kosong melompong. Tidak ada bintang, tidak ada daftar review, tidak ada tombol tulis review. Widget-nya diam saja. Dan yang bikin pusing: tidak ada error di console. Tidak ada 404 skrip, tidak ada exception JavaScript merah. Elemennya cuma duduk inert seolah tidak ada satu pun kode Judge.me yang tahu dia harus mengurusnya.
Mengejar yang salah
Refleks pertama saya: pasti datanya yang salah. Saya cek window.judgeme_review_widget_data, isinya ada. Saya cek product ID yang saya oper, benar. Saya bandingkan atribut div saya baris demi baris dengan docs, semua cocok. Saya sempat curiga loader Judge.me jalan sebelum div-nya ada, jadi saya utak-atik urutan load dan defer. Tetap kosong.
Karena buntu, saya ambil jalan pembanding. Saya buka wizard onboarding Judge.me di admin, lalu salin kode install Step 2-nya apa adanya ke section yang sama. Widget yang persis sama itu langsung render. Bintang, review, semuanya muncul dalam sekali refresh. Dua embed, produk yang sama, data yang sama, tapi yang satu diam dan yang satu hidup. Bedanya ada di markup yang digenerate wizard, bukan di data saya.
Akar masalahnya
Saya sandingkan dua markup itu berdampingan, dan selisihnya bukan kosmetik. Embed manual saya salah di beberapa detail implementasi yang tidak ada di dokumentasi publik yang saya pakai:
- Atribut yang benar adalah
data-widget='review', bukandata-widget='widget'. Nilaiwidgetyang saya salin dari docs tidak dikenali oleh bundle JS yang benar-benar terpasang di shop ini. - Ada dua atribut yang wajib ada supaya JS Judge.me mau nyantol ke elemen:
data-entry-point='review_widget.js'dandata-entry-key='review-widget/main.js'. Tanpa keduanya, script tidak pernah mem-bind widget, dan itu sebabnya tidak ada error. Dari sisi Judge.me, elemen saya bukan target yang valid, jadi dia dilewati diam-diam. - Hitungan review global datang dari metafield.
data-shop-reviews-countdiisi darishop.metafields.judgeme.shop_reviews_count, bukan angka yang saya karang. - Namespace hidrasinya juga beda. Data review tidak dibaca dari
window.judgeme_review_widget_data, melainkan darijdgm.data.reviewWidget[id]. Jadi meski data saya ada, dia ada di tempat yang salah dan tidak pernah dibaca.
Yang bikin ini menyesatkan: bundle JS Judge.me punya legacy fallback yang gate-nya ke widget.size > 20. Jadi di sebagian shop, embed gaya lama kelihatan jalan, dan itu yang bikin dokumentasi dan tutorial lama tampak benar. Kenyataannya, atribut dan namespace ini berubah antar versi widget, dan wizard onboarding meng-generate kode yang cocok dengan bundle JS yang persis terpasang di shop itu. Docs publik yang saya ikuti menggambarkan versi yang berbeda dari yang benar-benar dijalankan shop klien.
Jadi ini bukan bug di data saya. Datanya benar. Yang salah adalah kontrak markup: saya menebak nama atribut dan namespace dari dokumentasi umum, padahal yang mengikat semuanya adalah detail versi-spesifik yang hanya diketahui oleh wizard.
Perbaikannya
Perbaikannya justru anti-klimaks: berhenti merakit sendiri. Saya tempel kode install dari wizard apa adanya, komentar dan semua, tanpa merapikan nama atribut atau namespace. Satu-satunya yang saya tambahkan adalah pembungkus kondisional konteks template Liquid, supaya blok ini hanya muncul di tempat yang benar.
<div id='judgeme_product_reviews' class='jdgm-widget jdgm-review-widget'
data-product-title='{{ product.title | escape }}'
data-id='{{ product.id }}'
data-product-id='{{ product.id }}'
data-widget='review'
data-auto-install='false'
data-shop-reviews-count='{{ shop.metafields.judgeme.shop_reviews_count | default: 0 | escape }}'
data-entry-point='review_widget.js'
data-entry-key='review-widget/main.js'>
</div>
{% if product.metafields.judgeme.review_widget_data %}
<script>jdgm.data ||= {}; jdgm.data.reviewWidget ||= {}; jdgm.data.reviewWidget[{{ product.id }}] = {{ product.metafields.judgeme.review_widget_data }}</script>
{% endif %}Perhatikan hidrasinya: data review disuntik ke jdgm.data.reviewWidget[product.id], bukan ke variabel global karangan saya. Dan data-entry-point plus data-entry-key yang tadinya saya kira dekoratif ternyata itulah yang bikin JS-nya mau bekerja.
Hasil akhirnya: section yang tadinya 414 baris penuh override manual dan tebakan menyusut jadi 75 baris memakai embed resmi. Widget-nya render di sekali muat, dan yang lebih penting, dia tidak akan pecah diam-diam saat Judge.me merilis versi widget baru, karena saya tidak lagi mengunci diri ke nama atribut satu versi tertentu.
Pelajaran
- Kalau widget vendor diam tanpa error di console, curigai kontrak markup, bukan datamu. Elemen yang tidak dikenali dilewati tanpa suara.
- Untuk Judge.me Review Widget, atribut yang benar
data-widget='review', dandata-entry-pointplusdata-entry-keywajib ada supaya JS-nya mau bind. - Hidrasi lewat
jdgm.data.reviewWidget[id], bukanwindow.judgeme_review_widget_data. Data di namespace yang salah sama saja tidak pernah dibaca. - Detail ini versi-spesifik: wizard onboarding meng-generate markup yang cocok dengan bundle JS yang terpasang. Docs publik bisa menggambarkan versi lain.
- Kalau ada embed resmi dari vendor, tempel apa adanya. Jangan rapikan nama atribut atau namespace. Bungkus hanya dengan kondisional template kalau perlu.
