← back to Dw Contact Us Pages
layout: contact block sits under the image via a scoped 2-col grid (float rules measured); verify.mjs scoped assertions + control flag; README evidence
5d1534b668d7f244bce53850855358b2bcf0bcb1 · 2026-09-19 10:15:40 -0700 · Claude (TK-11925)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0163KBzeE1R39RSbxNmAjbki
Files touched
M README.mdA data/theme-preimage/145556635699/manifest.jsonA data/theme-preimage/145556635699/snippets/hide-browse-hidden.liquidA data/theme-preimage/145556635699/snippets/product-list-item.liquidA data/theme-preimage/145556635699/snippets/product.liquidA data/verify-latest.jsonM scripts/verify.mjsM theme/snippets/dw-contact-us-block.liquid
Diff
commit 5d1534b668d7f244bce53850855358b2bcf0bcb1
Author: Claude (TK-11925) <steve@designerwallcoverings.com>
Date: Sat Sep 19 10:15:40 2026 -0700
layout: contact block sits under the image via a scoped 2-col grid (float rules measured); verify.mjs scoped assertions + control flag; README evidence
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0163KBzeE1R39RSbxNmAjbki
---
README.md | 45 +++-
data/theme-preimage/145556635699/manifest.json | 50 ++++
.../snippets/hide-browse-hidden.liquid | 85 +++++++
.../145556635699/snippets/product-list-item.liquid | 269 +++++++++++++++++++++
.../145556635699/snippets/product.liquid | 106 ++++++++
data/verify-latest.json | 161 ++++++++++++
scripts/verify.mjs | 51 +++-
theme/snippets/dw-contact-us-block.liquid | 21 +-
8 files changed, 778 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 1780a78..9289d13 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,18 @@ mechanism — CSS is only a backstop.
3. **`theme/snippets/product.liquid`** — the live file, one additive block: right after
`{% render 'product-gallery' %}` it renders the contact block, so it sits directly
**below the image column** and above `.product-details-wrapper`.
+
+ *Layout note (measured, not guessed).* `theme.css` builds the PDP as a **float**
+ two-column at `>=770px`: `.product-images{float:left;width:50%}` /
+ `.product-details-wrapper{float:right;width:50%}`; the rendered gallery root is
+ `<div class="product-images product-gallery">`. A `float:left; clear:left` block
+ inserted between them does **not** work — CSS 2.1 forbids a later float
+ (`.product-details-wrapper`, `float:right`) from sitting higher than an earlier one,
+ so clearing under the image would drag the entire details column down with it.
+ Instead, **on `body.template-suffix-contact-us` only**, `.product` becomes a 2-column
+ grid: image top-left, contact block directly beneath it, details column spanning both
+ rows on the right. Below 770px the theme is single-column and the block falls into
+ normal flow right under the image. Nothing outside the contact-us template is touched.
4. **`theme/assets/dw-contact-us-cards.js`** + **`theme/snippets/hide-browse-hidden.liquid`**
— Boost SD renders browse/search grids client-side. The asset sweeps
`.boost-sd__product-item`, identifies the cohort by the card's
@@ -145,13 +157,44 @@ published for all 785. **Online Store stays published**: the PDP *is* the contac
Facebook & Instagram, Pinterest, TikTok, Houzz, Fabricut, POS, Inbox, Rakuten and
DWAutoPostBlog are deliberately untouched.
+## verify.mjs — what it measures, and what it refuses to call green
+
+`node scripts/verify.mjs [--n 5] [--control <handle>]` samples N products per vendor and
+reports **PASS / FAIL / NOT-MEASURED** per check (admin `templateSuffix`, variant
+hardening, sample-variant presence, the 3 channels, Online Store still on, live PDP HTML,
+public `.json` availability). `0 of 0` is **NOT-MEASURED**, never PASS — which is how the
+48 sample-only products are reported. Verdict is `FAIL` if anything failed, `WARN` if
+anything was unmeasured, `PASS` only when everything was measured and good. Result is
+written to `data/verify-latest.json`.
+
+**Scope matters, and it was measured.** On a PDP that already has *no* buy box (a Newmor
+showroom product) the literal strings `Add to cart` (x4), `dl-sample-btn` (x4) and
+`Complimentary Sample` (x1) still appear in the document — locale JSON, quick-shop and
+recommendation templates. A whole-document match therefore reports FAIL on a page that is
+already correct. Scoped to the product-details region those same tokens score **0**, while
+a buy-box page scores non-zero. Both polarities were run before shipping:
+
+```
+control bark-commercial-wallcovering-bark-vbk-102: scope=59430B buybox_markup=false # clean page
+control dinetah-stripe-indigo: scope=65313B buybox_markup=true # buy-box page
+```
+
+A truncated scope extraction returns `null` → NOT-MEASURED, so a broken extractor cannot
+score 0 and fake a PASS. Run `verify.mjs` **before** applying too: it must come back FAIL.
+It does (46 failures on the untouched cohort) — that is the negative test on the detector.
+
+`push-theme.mjs --apply` against the live main theme without `--allow-main` exits **2**
+before any PUT; that negative test was run and is the guard's proof.
+
## Residual risks
1. **`/products/<handle>.json` and the storefront product JSON still expose variant price
strings** even when `available:false`. Shopify offers no way to suppress that for a
published product. Price is hidden from the *page*, not from the public JSON endpoint.
Same for `oembed`/structured-data consumers that read that JSON.
-2. **The Boost grid treatment depends on client-side JS.** No-JS crawlers and the moment
+2. **The Boost grid treatment depends on client-side JS.** `verify.mjs` cannot assert it
+ from raw HTML and says so explicitly rather than staying silent.
+ No-JS crawlers and the moment
before the sweep runs will still show the Boost price. The Liquid card path
(`product-list-item.liquid`) has no such gap, but Boost renders most grids.
3. **The `body.template-suffix-contact-us` hook** comes from `layout/theme.liquid`
diff --git a/data/theme-preimage/145556635699/manifest.json b/data/theme-preimage/145556635699/manifest.json
new file mode 100644
index 0000000..602e965
--- /dev/null
+++ b/data/theme-preimage/145556635699/manifest.json
@@ -0,0 +1,50 @@
+{
+ "themeId": "145556635699",
+ "role": "main",
+ "name": "DW Sample-Shipping DEV",
+ "captured_at": "2026-09-19T17:10:31.981Z",
+ "files": [
+ {
+ "key": "assets/dw-contact-us-cards.js",
+ "created": true,
+ "identical": false,
+ "remote_bytes": 0,
+ "local_bytes": 4416
+ },
+ {
+ "key": "snippets/dw-contact-us-block.liquid",
+ "created": true,
+ "identical": false,
+ "remote_bytes": 0,
+ "local_bytes": 13502
+ },
+ {
+ "key": "snippets/hide-browse-hidden.liquid",
+ "created": false,
+ "identical": false,
+ "remote_bytes": 5319,
+ "local_bytes": 6635
+ },
+ {
+ "key": "snippets/product-list-item.liquid",
+ "created": false,
+ "identical": false,
+ "remote_bytes": 11087,
+ "local_bytes": 11670
+ },
+ {
+ "key": "snippets/product.liquid",
+ "created": false,
+ "identical": false,
+ "remote_bytes": 3157,
+ "local_bytes": 3619
+ },
+ {
+ "key": "templates/product.contact-us.json",
+ "created": true,
+ "identical": false,
+ "remote_bytes": 0,
+ "local_bytes": 2220
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/theme-preimage/145556635699/snippets/hide-browse-hidden.liquid b/data/theme-preimage/145556635699/snippets/hide-browse-hidden.liquid
new file mode 100644
index 0000000..5f19aa4
--- /dev/null
+++ b/data/theme-preimage/145556635699/snippets/hide-browse-hidden.liquid
@@ -0,0 +1,85 @@
+{% comment %}
+ Suppress showroom-only vendors from ALL browse/search grids. TK-11186.
+
+ DATA-DRIVEN — no vendor name is hardcoded in logic. The showroom-vendor list is
+ sourced (in priority order) from:
+ 1. shop metafield custom.showroom_vendors (comma-separated; store-wide, all themes)
+ 2. theme setting settings.showroom_vendors (comma-separated)
+ 3. a canonical fallback snapshot of ~/Projects/fix-live-board/config/showroom-vendors.json
+ (last resort so suppression never silently fails if 1+2 are unset).
+ Canonical source of truth = showroom-vendors.json. Set the shop metafield to that
+ file's contents (metafield-update skill) and this snippet follows it automatically.
+
+ Two layers:
+ (a) RENDER-TIME: product-list-item.liquid emits NO card HTML for a showroom vendor
+ (the real fix — no SEO-crawlable markup, no flash). This snippet's CSS also
+ hides any card that carries data-vendor at paint time (before JS).
+ (b) BACKSTOP: a JS observer catches Boost AJAX-rendered grids that bypass Liquid,
+ reading the SAME injected list (case-insensitive). Kept minimal.
+{% endcomment %}
+{%- liquid
+ assign showroom_raw = shop.metafields.custom.showroom_vendors
+ if showroom_raw == blank
+ assign showroom_raw = settings.showroom_vendors
+ endif
+ if showroom_raw == blank
+ assign showroom_raw = 'Phillip Jeffries'
+ endif
+ assign showroom_vendors = showroom_raw | split: ','
+-%}
+<style>
+{%- for v in showroom_vendors -%}
+{%- assign vt = v | strip -%}
+{%- if vt != blank -%}
+[data-vendor="{{ vt | escape }}"]{display:none!important;}
+{%- endif -%}
+{%- endfor -%}
+/* TK-11307: product-level 'Showroom' tag hook — a shared-vendor showroom line (MDC under
+ 'Phillipe Romano') carries this attr/class without exposing a hideable vendor name. */
+[data-showroom="true"],.pr-showroom-hidden{display:none!important;}
+</style>
+<script>
+(function(){
+ var SHOWROOM_TAG = 'showroomonly'; // TK-11307 — mirrors fix-live-board/config/showroom-vendor.cjs SHOWROOM_TAG
+ var HIDDEN = [{%- for v in showroom_vendors -%}{%- assign vt = v | strip -%}{%- if vt != blank -%}{{ vt | json }}{%- unless forloop.last -%},{%- endunless -%}{%- endif -%}{%- endfor -%}].map(function(s){return String(s).trim().toLowerCase();});
+ if (!HIDDEN.length) return;
+ var CARD_SEL = '.product-list-item, .boost-sd__product-item, [class*="product-item"], [class*="product-card"], article, .boost-sd__product';
+ function isHidden(text){ text = (text||'').trim().toLowerCase(); for (var i=0;i<HIDDEN.length;i++){ if (text.indexOf(HIDDEN[i]) > -1) return true; } return false; }
+ function hideProducts(){
+ document.querySelectorAll('.product-vendor, .product-list-item-vendor, [class*="vendor"]').forEach(function(el){
+ if (isHidden(el.textContent)){ var c = el.closest(CARD_SEL); if (c && c.style.display !== 'none') c.style.display = 'none'; }
+ });
+ document.querySelectorAll('.product-list-item-title a, .boost-sd__product-title a, [class*="product-title"] a, h2 a, h3 a').forEach(function(el){
+ if (isHidden(el.textContent)){ var c = el.closest(CARD_SEL); if (c && c.style.display !== 'none') c.style.display = 'none'; }
+ });
+ document.querySelectorAll('[data-vendor]').forEach(function(el){
+ if (HIDDEN.indexOf(String(el.getAttribute('data-vendor')||'').trim().toLowerCase()) > -1){ el.style.display = 'none'; }
+ });
+ // TK-11307: the product-level showroom hide is NOT done here.
+ // A data-tags hook used to live in this function. It was dead code — no product card on
+ // this store emits data-tags (measured: 59 cards across 4 surfaces, 0 data-tags) — and it
+ // was actively dangerous, because an earlier revision split tags on WHITESPACE and matched
+ // 'showroom', which makes the unrelated 'Showroom Line' tag on 18,518+ ACTIVE SELLABLE
+ // products (Kravet, China Seas, Scalamandre, Osborne & Little, sellable Phillipe Romano...)
+ // tokenize to ['showroom','line'] and MATCH. Leaving a dead hook in place invites someone
+ // to "make it work" by emitting data-tags, which arms exactly that landmine.
+ // Browse grids here are rendered by Boost SD, not Liquid, so the working mechanism is the
+ // id-keyed asset included below — see assets/dw-showroom-hide.js.
+ }
+ hideProducts();
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', hideProducts);
+ setInterval(hideProducts, 2000);
+ if (document.body){ new MutationObserver(function(){ setTimeout(hideProducts, 100); }).observe(document.body, { childList:true, subtree:true }); }
+})();
+</script>
+
+{%- comment -%}
+ TK-11307 — product-level showroom-only hide for Boost-rendered browse grids.
+ Boost renders every browse/search grid client-side, so neither product-list-item.liquid nor
+ the observer above can reach those cards; a rendered Boost card exposes only data-product-id
+ (no tags). dw-showroom-hide.js is GENERATED from the live 'ShowroomOnly' tag by
+ scripts/tk11307-showroom-tag/build-showroom-hide-asset.mjs and matches on that id.
+ It deliberately no-ops on /search and ?q= so the line stays findable on-site — Steve's rule
+ is "addressable but not discoverable". Regenerate the asset whenever the tagged set changes.
+{%- endcomment -%}
+<script src="{{ 'dw-showroom-hide.js' | asset_url }}" defer></script>
diff --git a/data/theme-preimage/145556635699/snippets/product-list-item.liquid b/data/theme-preimage/145556635699/snippets/product-list-item.liquid
new file mode 100644
index 0000000..6aa3c27
--- /dev/null
+++ b/data/theme-preimage/145556635699/snippets/product-list-item.liquid
@@ -0,0 +1,269 @@
+{% comment %}
+ @param product_attributes
+ custom attributes to be applied to the product item
+ Defaults to: blank
+{% endcomment %}
+
+{%- comment %} TK-11186 render-time showroom-vendor skip: emit NO card HTML for a showroom-only
+ vendor (addressable-not-discoverable). List sourced from shop metafield / theme setting /
+ canonical showroom-vendors.json fallback — never a hardcoded vendor. {% endcomment -%}
+{%- liquid
+ assign showroom_raw = shop.metafields.custom.showroom_vendors
+ if showroom_raw == blank
+ assign showroom_raw = settings.showroom_vendors
+ endif
+ if showroom_raw == blank
+ assign showroom_raw = 'Phillip Jeffries'
+ endif
+ assign showroom_vendors = showroom_raw | split: ','
+ assign this_vendor = product.vendor | strip | downcase
+ assign is_showroom_vendor = false
+ for sv in showroom_vendors
+ assign svn = sv | strip | downcase
+ if svn != blank and svn == this_vendor
+ assign is_showroom_vendor = true
+ endif
+ endfor
+ # TK-11307: product-level 'Showroom' TAG is an ADDITIONAL, shared-vendor-safe key.
+ # A showroom line under a SHARED private label (MDC under 'Phillipe Romano', which also
+ # carries sellable lines) is suppressed by its own tag WITHOUT hiding the vendor. The
+ # tag applier stamps 'Showroom' on exactly the showroom products; sellable siblings keep
+ # rendering. Case-insensitive: check both 'Showroom' and 'showroom'.
+ if product.tags contains 'Showroom' or product.tags contains 'showroom'
+ assign is_showroom_vendor = true
+ endif
+-%}
+{%- unless is_showroom_vendor -%}
+
+{% liquid
+ assign product_attributes = product_attributes | default: ''
+ assign product_hover = settings.product_hover | default: 'quick-shop'
+ assign product_stock_level_threshold = settings.product_stock_level_threshold | default: 1
+ assign product_badges = settings.product_badges | default: false
+ assign product_icons = settings.product_badges_icons | default: false
+ assign product_vendor = settings.show_vendor | default: false
+
+ assign item = product
+ if template contains 'search'
+ assign item = item
+ endif
+
+ assign has_quick_shop = false
+ if product_hover == 'quick-shop' and template.name != 'password' and product.variants_count <= 250 and product.vendor != 'Newmor Wallcoverings'
+ assign has_quick_shop = true
+ endif
+%}
+
+{% if product_hover == 'stock-level' and item.available %}
+ {% assign total = 0 %}
+ {% assign threshold = product_stock_level_threshold | times: 1 %}
+ {% assign infinity = false %}
+ {% for variant in item.variants %}
+ {% if variant.inventory_management == null %}
+ {% assign infinity = true %}
+ {% elsif variant.inventory_management == '' %}
+ {% assign infinity = true %}
+ {% elsif variant.inventory_management == 'shopify' and variant.inventory_policy == 'continue' %}
+ {% assign infinity = true %}
+ {% elsif infinity == false %}
+ {% capture temp %}{{ total | plus: variant.inventory_quantity }}{% endcapture %}
+ {% assign total = temp | times: 1 %}
+ {% endif %}
+ {% endfor %}
+ {% assign stockText = 'products.product.stock_indicator_message' | t: num: total %}
+{% endif %}
+
+<article
+ class="
+ product-list-item
+ {% if has_quick_shop %} has-quick-shop{% endif %}
+ {% if item.available and infinity == false and total <= threshold %} has-stock-indicator{%endif %}
+ "
+ id="product-list-item-{{ item.id }}"
+ data-product-id="{{ item.id }}"
+ {{ product_attributes }}
+>
+
+ {% assign secondaryImage = false %}
+ {% if item.media.size > 1 and product_hover == 'image-flip' %}
+ {% assign secondaryImage = true %}
+ {% endif %}
+
+ <figure
+ class="
+ product-list-item-thumbnail
+ {% if secondaryImage %}
+ has-secondary-image
+ {% endif %}
+ "
+ data-url="{{ item.url | within: collection }}"
+ {% if secondaryImage %}
+ {%
+ render 'rimg',
+ img: item.media[1].preview_image,
+ alt: item.media[1].preview_image.alt,
+ size: '600x600',
+ background: true,
+ lazy: true
+ %}
+ {% endif %}
+ >
+ <a href="{{ item.url | within: collection }}" aria-label="{{ item.title }}">
+ {% if item.featured_media.preview_image %}
+ {%
+ render 'rimg',
+ img: item.featured_media.preview_image,
+ alt: item.featured_media.preview_image.alt,
+ size: '600x600',
+ lazy: true
+ %}
+ {% else %}
+ {{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}
+ {% endif %}
+
+ <div class="product-info">
+ <div class="vertical-center">
+ <div>View Pattern</div>
+ {%- liquid
+ assign sku_raw = item.variants.last.sku | default: ''
+ assign sku_display = sku_raw | replace: '-Sample', '' | replace: '-SAMPLE', '' | replace: '-sample', ''
+ assign sku_display = sku_display | replace: '--', '-' | replace: '--', '-'
+ assign sku_last_char = sku_display | slice: -1, 1
+ if sku_display != blank and sku_last_char == '-'
+ assign sku_display = sku_display | remove_last: '-'
+ endif
+ -%}
+ <div>{{ sku_display }}</div>
+ </div>
+ </div>
+
+ </a>
+
+ {% if has_quick_shop %}
+ <span
+ class="quick-shop-modal-trigger"
+ data-product-url="{{ item.url | within: collection }}"
+ >
+ {{ 'products.product.quick_shop_trigger_text' | t }}
+ </span>
+ {% elsif product_hover == 'stock-level' %}
+
+ {% if item.available and infinity == false and total <= threshold %}
+ <a class="product-list-item-inventory" href="{{ item.url }}">{{ stockText }}</a>
+ {% endif %}
+
+ {% endif %}
+
+ {% if product_badges %}
+ {% if item.available != true %}
+ <span class="product-list-item-unavailable{% if product_icons %} product-icons{% endif %}" data-title="{{ 'products.product.sold_out' | t }}"></span>
+ {% elsif item.compare_at_price_min > item.price_min %}
+ <span class="product-list-item-on-sale{% if product_icons %} product-icons{% endif %}" data-title="{{ 'products.product.on_sale' | t }}"></span>
+ {% endif %}
+ {% endif %}
+ </figure>
+
+ <div class="product-list-item-details">
+ {% if product_vendor %}
+ <p class="product-list-item-vendor">{{ item.vendor | link_to_vendor }}</p>
+ {% endif %}
+ <h2 class="product-list-item-title"><a href="{{ item.url | within: collection }}">{{ item.title }}</a></h2>
+
+ {%- comment -%}
+ DW grid-card color swatch (Carnegie-restructure fix, TK-10686).
+ Additive: renders ONLY when a valid hex metafield exists, so non-Carnegie
+ cards are unaffected. Reads hex + name across the lowercase, space-cased,
+ and swatch_hex key variants so the Carnegie Xorel restructure is covered.
+ Pure Liquid, fixed dimensions (no CLS), no shared DOM ids (grid-safe).
+ {%- endcomment -%}
+ {%- liquid
+ assign dw_hex = item.metafields.custom.color_hex.value | default: item.metafields.custom['Color Hex'].value | default: item.metafields.custom.swatch_hex.value | default: item.metafields.custom.primary_color_hex.value
+ assign dw_color_name = item.metafields.custom.color_name.value | default: item.metafields.custom.color.value | default: item.metafields.custom['Color'].value | default: item.metafields.global['Color'].value
+ assign dw_hex_body = dw_hex | strip | remove_first: '#'
+ assign dw_hex_len = dw_hex_body | size
+ assign dw_hex_ok = false
+ if dw_hex_len == 6
+ assign dw_hex_ok = true
+ elsif dw_hex_len == 3
+ assign dw_hex_ok = true
+ endif
+ -%}
+ {%- if dw_hex_ok -%}
+ <p class="product-list-item-swatch" style="display:flex;align-items:center;gap:8px;margin:4px 0 2px;line-height:1;">
+ <span class="product-list-item-swatch-dot" aria-hidden="true" style="display:inline-block;width:16px;height:16px;min-width:16px;border-radius:50%;background:#{{ dw_hex_body | escape }};border:1px solid rgba(0,0,0,0.15);box-shadow:0 1px 2px rgba(0,0,0,0.08);flex:0 0 auto;"></span>
+ {%- if dw_color_name != blank -%}
+ <span class="product-list-item-swatch-name" style="font-size:0.82em;color:#555;">{{ dw_color_name | escape }}</span>
+ {%- endif -%}
+ </p>
+ {%- endif -%}
+
+ {% if item.vendor == 'Newmor Wallcoverings' %}
+ <p class="product-list-item-price" data-newmor-showroom>Showroom inquiry</p>
+ {% else %}
+ <p class="product-list-item-price">
+ {% if item.price_varies %}
+ {% if item.price_varies %}{{ 'products.product.from' | t }}{% endif %}
+ {% if item.compare_at_price_min > item.price_min %}
+ <span class="money">{{ item.price_min | money }}</span>
+ <span class="original money">{{ item.compare_at_price_min | money }}</span>
+ {% else %}
+ <span class="money">{{ item.price_min | money }}</span>
+ {% endif %}
+ {% else %}
+ {% if item.compare_at_price_min > item.price_min %}
+ <span class="money">{{ item.price_min | money }}</span>
+ <span class="original money">{{ item.compare_at_price_min | money }}</span>
+ {% else %}
+ <span class="money">{{ item.price_min | money }}</span>
+ {% endif %}
+ {% endif %}
+ </p>
+ {% assign variant_for_unit_price = item.variants | sort: 'price' | first %}
+ {% if variant_for_unit_price.unit_price %}
+ {% comment %}Inject unit-price begin{% endcomment %}
+ {% comment %}
+ @param variant_for_unit_price
+ Product variant for price
+ @param tax_text
+ String containing 'tax included' text
+ {% endcomment %}
+
+ {% capture total_quantity %}
+ <span class="product-price__unit-price-total-quantity" data-unit-price-quantity>
+ {{ variant_for_unit_price.unit_price_measurement.quantity_value }}{{ variant_for_unit_price.unit_price_measurement.quantity_unit }}
+ </span>
+ {% endcapture %}
+
+
+ {% capture unit_price %}
+ <span class="product-price__unit-price-amount money" data-unit-price-amount>
+ {{ variant_for_unit_price.unit_price | money }}
+ </span>
+ {% endcapture %}
+ {% capture unit_measure %}
+ <span class="product-price__unit-price-measure" data-unit-price-measure>
+ {%- if variant_for_unit_price.unit_price_measurement.reference_value != 1 -%}
+ {{ variant_for_unit_price.unit_price_measurement.reference_value }}
+ {%- endif %}
+ {{ variant_for_unit_price.unit_price_measurement.reference_unit }}
+ </span>
+ {% endcapture %}
+
+ <div
+ class="
+ product-price__unit-price
+ {% unless variant_for_unit_price.unit_price_measurement %}hidden{% endunless %}
+ "
+ data-unit-price
+ >
+ {{ 'products.product.price_per_unit_html' | t: total_quantity: total_quantity, unit_price: unit_price, unit_measure: unit_measure | strip_newlines }}
+ </div>
+
+ {% assign variant_for_unit_price = blank %}
+ {% comment %}Inject unit-price end{% endcomment %}
+
+ {% endif %}
+ {% endif %}
+ </div>
+
+</article>{%- endunless -%}
diff --git a/data/theme-preimage/145556635699/snippets/product.liquid b/data/theme-preimage/145556635699/snippets/product.liquid
new file mode 100644
index 0000000..5094666
--- /dev/null
+++ b/data/theme-preimage/145556635699/snippets/product.liquid
@@ -0,0 +1,106 @@
+{%- comment -%}cache-bump 2026-08-03T16:43:43.170Z force PDP HTML regen for hero-freeze CSS{%- endcomment -%}
+{% assign use_masonry = false %}
+{% assign link_to = link_to | default: nil %}
+
+<script type="application/json" data-images>
+ {% assign product_images = product.media | where: 'media_type', 'image' %}
+
+ {
+ {%- for media in product_images %}
+ {% if media.media_type == 'image' %}
+ {%- capture rimg_string -%}
+ {%
+ render 'rimg',
+ img: media,
+ attr: 'data-product-main-image',
+ size: '1400x',
+ lazy: false
+ %}
+ {%- endcapture -%}
+ "{{ media.id }}": {{ rimg_string | strip_newlines | json }}{%- unless forloop.last %},{%- endunless -%}
+ {% endif %}
+ {%- endfor -%}
+ }
+</script>
+
+{% assign product_id = product.id %}
+{% assign selected_variant = product.selected_or_first_available_variant %}
+
+{% if collection == null or collection.handle == 'frontpage' or collection.handle == 'all' %}
+ {% assign found_a_collection = false %}
+ {% for c in product.collections %}
+ {% if found_a_collection == false and c.handle != 'frontpage' and c.handle != 'all' %}
+ {% assign found_a_collection = true %}
+ {% assign collection = c %}
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+{% assign selected_media = selected_variant.featured_media | default: product.featured_media %}
+
+<div
+ class="
+ product
+ {% if images_layout == 'masonry' %}
+ product-masonry
+ {% endif %}
+ "
+ data-ajax
+
+ {% if enable_zoom %}
+ data-zoom
+ {% endif %}
+
+ {% if enable_linked_options %}
+ data-linked-options
+ {% endif %}
+
+ {% if images_layout == 'slideshow' %}
+ data-images-slideshow
+ {% elsif images_layout == 'list' %}
+ data-images-list-view
+ {% elsif images_layout == 'masonry' %}
+ {% assign use_masonry = true %}
+ data-product-masonry
+ {% endif %}
+>
+ {%
+ render 'product-gallery',
+ images_layout: images_layout,
+ use_masonry: use_masonry,
+ product: product,
+ selected_media: selected_media,
+ enable_zoom: enable_zoom,
+ %}
+
+ <div class="product-details-wrapper">
+ <div class="product-details">
+ {% if product.vendor == 'Newmor Wallcoverings' %}
+ <div data-product-form>
+ {% render 'product-form-content', product: product, form: nil, show_social_media_icons: show_social_media_icons, show_payment_button: false %}
+ </div>
+ {% elsif product != blank %}
+ {% form 'product', product, data-product-form: '' %}
+ {%
+ render 'product-form-content',
+ product: product,
+ form: form,
+ show_social_media_icons: show_social_media_icons,
+ show_payment_button: show_payment_button,
+ %}
+ {% endform %}
+ {% else %}
+ <div data-product-form>
+ {%
+ render 'product-form-content',
+ product: nil,
+ form: nil,
+ show_social_media_icons: show_social_media_icons,
+ show_payment_button: show_payment_button,
+ %}
+ </div>
+ {% endif %}
+ </div>
+ {% render 'product-description-meta' %}
+ </div>
+</div>
diff --git a/data/verify-latest.json b/data/verify-latest.json
new file mode 100644
index 0000000..6a45ea0
--- /dev/null
+++ b/data/verify-latest.json
@@ -0,0 +1,161 @@
+{
+ "ts": "2026-09-19T17:12:49.578Z",
+ "verdict": "FAIL",
+ "fail": 23,
+ "not_measured": 2,
+ "population": 785,
+ "observed": 3,
+ "storefront": "https://www.designerwallcoverings.com",
+ "rows": [
+ {
+ "handle": "chennai-cobalt-blue-wallpaper",
+ "vendor": "Designers Guild",
+ "checks": {
+ "templateSuffix": {
+ "verdict": "FAIL",
+ "detail": "suffix=\"\""
+ },
+ "variants_hardened": {
+ "verdict": "NOT-MEASURED",
+ "detail": "0/0 non-sample DENY+tracked+qty<=0 (sample-only product: 0 of 0)"
+ },
+ "sample_untouched": {
+ "verdict": "PASS",
+ "detail": "1 sample variant(s) present"
+ },
+ "channels_unpublished": {
+ "verdict": "FAIL",
+ "detail": "still on: Buy Button, Shop, Google & YouTube"
+ },
+ "online_store_kept": {
+ "verdict": "PASS",
+ "detail": "published=true"
+ },
+ "contact_block": {
+ "verdict": "FAIL",
+ "detail": "dw-cu block in HTML"
+ },
+ "no_add_to_cart": {
+ "verdict": "FAIL",
+ "detail": "add-to-cart markup in product details"
+ },
+ "no_price_markup": {
+ "verdict": "FAIL",
+ "detail": "price/form markup in product details"
+ },
+ "no_sample_button": {
+ "verdict": "FAIL",
+ "detail": "sample UI in product details"
+ },
+ "body_class": {
+ "verdict": "FAIL",
+ "detail": "body.template-suffix-contact-us hook"
+ },
+ "json_rolls_unavailable": {
+ "verdict": "NOT-MEASURED",
+ "detail": "0/0 non-sample variants available:false"
+ }
+ }
+ },
+ {
+ "handle": "dinetah-stripe-indigo",
+ "vendor": "Ralph Lauren",
+ "checks": {
+ "templateSuffix": {
+ "verdict": "FAIL",
+ "detail": "suffix=\"\""
+ },
+ "variants_hardened": {
+ "verdict": "FAIL",
+ "detail": "0/1 non-sample DENY+tracked+qty<=0"
+ },
+ "sample_untouched": {
+ "verdict": "PASS",
+ "detail": "1 sample variant(s) present"
+ },
+ "channels_unpublished": {
+ "verdict": "FAIL",
+ "detail": "still on: Buy Button, Shop, Google & YouTube"
+ },
+ "online_store_kept": {
+ "verdict": "PASS",
+ "detail": "published=true"
+ },
+ "contact_block": {
+ "verdict": "FAIL",
+ "detail": "dw-cu block in HTML"
+ },
+ "no_add_to_cart": {
+ "verdict": "FAIL",
+ "detail": "add-to-cart markup in product details"
+ },
+ "no_price_markup": {
+ "verdict": "FAIL",
+ "detail": "price/form markup in product details"
+ },
+ "no_sample_button": {
+ "verdict": "FAIL",
+ "detail": "sample UI in product details"
+ },
+ "body_class": {
+ "verdict": "FAIL",
+ "detail": "body.template-suffix-contact-us hook"
+ },
+ "json_rolls_unavailable": {
+ "verdict": "PASS",
+ "detail": "1/1 non-sample variants available:false"
+ }
+ }
+ },
+ {
+ "handle": "eur-80464-pcl004-designer-wallcoverings-los-angeles",
+ "vendor": "Christian Lacroix Europe",
+ "checks": {
+ "templateSuffix": {
+ "verdict": "FAIL",
+ "detail": "suffix=null"
+ },
+ "variants_hardened": {
+ "verdict": "FAIL",
+ "detail": "0/1 non-sample DENY+tracked+qty<=0"
+ },
+ "sample_untouched": {
+ "verdict": "PASS",
+ "detail": "1 sample variant(s) present"
+ },
+ "channels_unpublished": {
+ "verdict": "FAIL",
+ "detail": "still on: Buy Button, Shop, Google & YouTube"
+ },
+ "online_store_kept": {
+ "verdict": "PASS",
+ "detail": "published=true"
+ },
+ "contact_block": {
+ "verdict": "FAIL",
+ "detail": "dw-cu block in HTML"
+ },
+ "no_add_to_cart": {
+ "verdict": "FAIL",
+ "detail": "add-to-cart markup in product details"
+ },
+ "no_price_markup": {
+ "verdict": "FAIL",
+ "detail": "price/form markup in product details"
+ },
+ "no_sample_button": {
+ "verdict": "FAIL",
+ "detail": "sample UI in product details"
+ },
+ "body_class": {
+ "verdict": "FAIL",
+ "detail": "body.template-suffix-contact-us hook"
+ },
+ "json_rolls_unavailable": {
+ "verdict": "PASS",
+ "detail": "1/1 non-sample variants available:false"
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/scripts/verify.mjs b/scripts/verify.mjs
index f2794a7..ebe3853 100644
--- a/scripts/verify.mjs
+++ b/scripts/verify.mjs
@@ -12,6 +12,21 @@ const a = parseArgs();
const N = Number(a.n || 5);
const STORE = String(a.storefront || 'https://www.designerwallcoverings.com').replace(/\/$/, '');
+// Narrow the HTML to the product-details region. Everything price/cart/sample shaped
+// must be asserted INSIDE this window — see the note at the call site.
+function scopeDetails(html) {
+ const i = html.indexOf('product-details-wrapper');
+ if (i < 0) return null;
+ let j = html.length;
+ for (const e of ['static-product-recommendations', 'id="dw-similar"', 'You may also like', 'END sections: footer', '<footer']) {
+ const k = html.indexOf(e, i);
+ if (k > -1 && k < j) j = k;
+ }
+ const out = html.slice(i, j);
+ // A truncated extraction would score 0 on every token and fake a PASS.
+ return out.length < 1000 ? null : out;
+}
+
const targets = loadTargets();
const sample = [];
for (const v of VENDORS) sample.push(...targets.filter((p) => p.vendor === v).slice(0, N));
@@ -68,13 +83,24 @@ for (const p of sample) {
set(k, 'NOT-MEASURED', 'PDP HTML unavailable');
} else {
set('contact_block', /class="dw-cu"|dw-cu__h/.test(html) ? 'PASS' : 'FAIL', 'dw-cu block in HTML');
- const cart = /Add to cart|class="add-to-cart|name="add"/i.test(html);
- set('no_add_to_cart', cart ? 'FAIL' : 'PASS', cart ? 'add-to-cart markup present' : 'none');
- const details = (html.match(/<div class="product-details-wrapper">[\s\S]*?<\/div>\s*<\/div>/) || [''])[0] || '';
- const priced = /class="product__price"/.test(html) || /<span class="money">/.test(details);
- set('no_price_markup', priced ? 'FAIL' : 'PASS', priced ? 'price markup in product details' : 'none');
- const smpBtn = /dl-sample-btn|dl-second-sample-btn|Complimentary Sample/.test(html);
- set('no_sample_button', smpBtn ? 'FAIL' : 'PASS', smpBtn ? 'sample UI present' : 'none');
+ // SCOPE MATTERS. Measured on a known no-buy-box PDP (a Newmor showroom product):
+ // the literal strings "Add to cart" (x4), "dl-sample-btn" (x4) and "Complimentary
+ // Sample" (x1) appear elsewhere in the document — locale JSON, quick-shop and
+ // recommendation templates — so a whole-document match reports FAIL on a page that
+ // is already correct. Scoped to the product-details region, that same page scores 0
+ // on every token and a buy-box page scores non-zero: a real discriminator.
+ const details = scopeDetails(html);
+ if (details === null) {
+ for (const k of ['no_add_to_cart', 'no_price_markup', 'no_sample_button'])
+ set(k, 'NOT-MEASURED', 'product-details region not found in HTML');
+ } else {
+ const cart = /class="add-to-cart|Add to cart/i.test(details);
+ set('no_add_to_cart', cart ? 'FAIL' : 'PASS', cart ? 'add-to-cart markup in product details' : 'none in product details');
+ const priced = /class="product__price"|class="product__form"|class="money"|<span class="money">/.test(details);
+ set('no_price_markup', priced ? 'FAIL' : 'PASS', priced ? 'price/form markup in product details' : 'none in product details');
+ const smpBtn = /dl-sample-btn|dl-second-sample-btn|Complimentary Sample/.test(details);
+ set('no_sample_button', smpBtn ? 'FAIL' : 'PASS', smpBtn ? 'sample UI in product details' : 'none in product details');
+ }
set('body_class', /template-suffix-contact-us/.test(html) ? 'PASS' : 'FAIL', 'body.template-suffix-contact-us hook');
}
@@ -94,6 +120,17 @@ for (const p of sample) {
rows.push(r);
}
+// ---- positive control: a handle that SHOULD already be clean (no buy box) proves the
+// scoped extractor can still find markup when it is there, rather than always scoring 0.
+if (a.control) {
+ try {
+ const html = await (await fetch(`${STORE}/products/${a.control}`)).text();
+ const d = scopeDetails(html);
+ const hit = d === null ? null : /class="add-to-cart|class="product__price"|dl-sample-btn/.test(d);
+ console.log(`\ncontrol ${a.control}: scope=${d === null ? 'NOT-FOUND' : d.length + 'B'} buybox_markup=${hit}`);
+ } catch (e) { console.log(`control ${a.control}: NOT-MEASURED (${String(e).slice(0, 60)})`); }
+}
+
// ---- table
const cols = [...new Set(rows.flatMap((r) => Object.keys(r.checks)))];
const pad = (s, n) => String(s).padEnd(n);
diff --git a/theme/snippets/dw-contact-us-block.liquid b/theme/snippets/dw-contact-us-block.liquid
index c6d531d..bc67fdf 100644
--- a/theme/snippets/dw-contact-us-block.liquid
+++ b/theme/snippets/dw-contact-us-block.liquid
@@ -151,7 +151,24 @@
</section>
<style>
- .dw-cu{clear:both;margin:26px 0 8px;max-width:760px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#3D4246;box-sizing:border-box}
+ /* LAYOUT — measured, not guessed.
+ theme.css: @media(min-width:770px){.product-images{float:left;width:50%}} and
+ .product-details-wrapper{float:right;width:50%}; below 770px both go full width.
+ The rendered gallery root is <div class="product-images product-gallery">.
+ A float:left block inserted between them does NOT work: CSS 2.1 forbids a later
+ float (.product-details-wrapper, float:right) from sitting higher than an earlier
+ float, so clearing under the image would drag the whole details column down with it.
+ So on the contact-us template ONLY, .product becomes a 2-col grid: image top-left,
+ contact block directly beneath it, details column spanning both rows on the right.
+ Scoped to body.template-suffix-contact-us — zero effect on any other product. */
+ .dw-cu{margin:26px 0 8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#3D4246;box-sizing:border-box;clear:both}
+ @media(min-width:770px){
+ body.template-suffix-contact-us .product{display:grid;grid-template-columns:1fr 1fr;align-items:start}
+ body.template-suffix-contact-us .product > .product-images{grid-column:1;grid-row:1;float:none!important;width:auto!important}
+ body.template-suffix-contact-us .product > .dw-cu{grid-column:1;grid-row:2;float:none;width:auto;clear:none;padding:0 30px 0 55px}
+ body.template-suffix-contact-us .product > .product-details-wrapper{grid-column:2;grid-row:1 / span 2;float:none!important;width:auto!important}
+ }
+ @media(max-width:769px){.dw-cu{width:100%;padding:0 20px;text-align:left}}
.dw-cu *{box-sizing:border-box}
.dw-cu__h{font-family:Lora,serif;font-size:20px;font-weight:500;color:#3D4246;letter-spacing:-0.01em;margin:0 0 8px}
.dw-cu__meta{margin:0 0 6px;font-size:12px;line-height:1.6;color:#6b6357;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
@@ -181,7 +198,7 @@
.dw-cu__status{margin:.7rem 0 0;font-size:13px;text-align:center;border-radius:6px;padding:.6rem}
.dw-cu__status--ok{background:#eef6ee;color:#2f6b2f;border:1px solid #cfe4cf}
.dw-cu__status--err{background:#fdf1f1;color:#8b2f2f;border:1px solid #f0d4d4}
- @media(max-width:640px){.dw-cu{margin:20px 0 4px}.dw-cu__row{grid-template-columns:1fr}.dw-cu__direct .dw-cu__btn{flex:1 1 100%;text-align:center}}
+ @media(max-width:769px){.dw-cu{margin:20px 0 4px}.dw-cu__row{grid-template-columns:1fr}.dw-cu__direct .dw-cu__btn{flex:1 1 100%;text-align:center}}
/* ------------------------------------------------------------------ *
* BELT AND BRACES (TK-11925). The contact-us template omits the price *
← b61f5af verify.mjs (PASS/FAIL/NOT-MEASURED) + README with order of o
·
back to Dw Contact Us Pages
·
refresh live-theme preimage snapshot after layout CSS change b979aee →