On a Shopify client site, I was asked to drop a Judge.me reviews block onto the product page (PDP). Judge.me publishes public docs for its Review Widget embed, so I figured this was a ten-minute job: hand-roll the widget div in a Liquid section, fill in the data attributes the docs list, paste the hydration script, done. I built the embed by hand: a <div> carrying data-widget='widget', then fed it review data through window.judgeme_review_widget_data, exactly as I read it.
The result: nothing. The div was in the DOM, I could see it in the inspector, but it was completely empty. No stars, no review list, no write-a-review button. The widget just sat there. And the maddening part: no error in the console. No 404 on a script, no red JavaScript exception. The element sat inert as if no part of Judge.me's code even knew it was supposed to own it.
Chasing the wrong thing
My first instinct: the data must be wrong. I checked window.judgeme_review_widget_data, it was populated. I checked the product ID I passed, correct. I diffed my div's attributes line by line against the docs, all matched. I suspected the Judge.me loader was running before my div existed, so I fiddled with load order and defer. Still empty.
Stuck, I reached for a control. I opened the Judge.me onboarding wizard in the admin and copied its Step 2 install code verbatim into the same section. The exact same widget rendered instantly. Stars, reviews, all of it, in one refresh. Two embeds, same product, same data, but one was dead and one was alive. The difference lived in the markup the wizard generated, not in my data.
The root cause
I put the two blocks of markup side by side, and the delta was not cosmetic. My hand-rolled embed was wrong in several implementation details that were nowhere in the public docs I had followed:
- The correct attribute is
data-widget='review', notdata-widget='widget'. Thewidgetvalue I lifted from the docs was not recognized by the JS bundle actually installed on this shop. - Two attributes are required for the Judge.me JS to bind to the element:
data-entry-point='review_widget.js'anddata-entry-key='review-widget/main.js'. Without them, the script never binds the widget, and that is why there was no error. From Judge.me's side, my element was not a valid target, so it was skipped silently. - The global review count comes from a metafield.
data-shop-reviews-countis filled fromshop.metafields.judgeme.shop_reviews_count, not a number I invent. - The hydration namespace is different too. Review data is not read from
window.judgeme_review_widget_data; it is read fromjdgm.data.reviewWidget[id]. So even though my data existed, it existed in the wrong place and was never read.
Here is what made it so misleading: the Judge.me JS bundle carries a legacy fallback gated on widget.size > 20. On some shops, old-style embeds appear to work, which is exactly what makes stale docs and tutorials look correct. In reality, these attributes and namespaces change between widget versions, and the onboarding wizard generates code that matches the exact JS bundle installed on that shop. The public docs I followed described a different version than the one the client's shop was actually running.
So this was not a bug in my data. The data was right. What was wrong was the markup contract: I had guessed attribute names and namespaces from general documentation, when the thing that binds it all together is version-specific detail that only the wizard knows.
The fix
The fix was almost anticlimactic: stop hand-rolling it. I pasted the wizard's install code verbatim, comments and all, without tidying up attribute names or namespaces. The only thing I added was a Liquid template-context conditional so the block only appears where it should.
<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 %}Notice the hydration: review data is injected into jdgm.data.reviewWidget[product.id], not into some global variable I made up. And data-entry-point plus data-entry-key, which I had assumed were decorative, turned out to be exactly what makes the JS do its job.
The end result: a section that had ballooned to 414 lines of manual overrides and guesswork shrank to 75 lines using the official embed. The widget rendered on first load, and more importantly, it will not break silently the next time Judge.me ships a new widget version, because I am no longer pinning myself to one version's attribute names.
Takeaways
- When a vendor widget goes silent with no console error, suspect the markup contract, not your data. An unrecognized element gets skipped without a sound.
- For the Judge.me Review Widget, the correct attribute is
data-widget='review', anddata-entry-pointplusdata-entry-keyare required for the JS to bind. - Hydrate through
jdgm.data.reviewWidget[id], notwindow.judgeme_review_widget_data. Data in the wrong namespace is data that never gets read. - These details are version-specific: the onboarding wizard generates markup that matches the installed JS bundle. Public docs may describe a different version.
- If the vendor ships an official embed, paste it verbatim. Do not tidy attribute names or namespaces. Wrap it only in template conditionals if you must.
