← back to Designerwallcoverings
TK-11404: theme fix — correct emitters (theme.liquid JSON-LD + product-form-content microdata); apply-final + verify scripts
e5b18f7be8367f726f7558c7da0399cb9817e5ea · 2026-09-11 09:32:23 -0700 · Steve Abrams
Files touched
A scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__assets_dw-showroom-hide.jsA scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__snippets_hide-browse-hidden.liquidA scripts/tk11404-apply-theme-final.mjsA scripts/tk11404-theme-fix.mjsA scripts/tk11404-verify-live.mjs
Diff
commit e5b18f7be8367f726f7558c7da0399cb9817e5ea
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 09:32:23 2026 -0700
TK-11404: theme fix — correct emitters (theme.liquid JSON-LD + product-form-content microdata); apply-final + verify scripts
---
...09-11T15-19-23-535Z__assets_dw-showroom-hide.js | 0
...-19-23-535Z__snippets_hide-browse-hidden.liquid | 70 ++++++++++++
scripts/tk11404-apply-theme-final.mjs | 42 ++++++++
scripts/tk11404-theme-fix.mjs | 120 +++++++++++++++++++++
scripts/tk11404-verify-live.mjs | 40 +++++++
5 files changed, 272 insertions(+)
diff --git a/scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__assets_dw-showroom-hide.js b/scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__assets_dw-showroom-hide.js
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__snippets_hide-browse-hidden.liquid b/scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__snippets_hide-browse-hidden.liquid
new file mode 100644
index 0000000..193f8fe
--- /dev/null
+++ b/scripts/tk11186-showroom-hide/theme-deploy/preimages/2026-09-11T15-19-23-535Z__snippets_hide-browse-hidden.liquid
@@ -0,0 +1,70 @@
+{% 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 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: hide any card whose product tags include 'Showroom' (shared-vendor path).
+ // Covers Boost/AJAX grids where the card exposes tags via a data-tags attribute.
+ document.querySelectorAll('[data-tags]').forEach(function(el){
+ if (String(el.getAttribute('data-tags')||'').toLowerCase().split(/[,\s]+/).indexOf('showroom') > -1){
+ var c = el.closest(CARD_SEL) || el; if (c && c.style.display !== 'none') c.style.display = 'none';
+ }
+ });
+ }
+ 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>
diff --git a/scripts/tk11404-apply-theme-final.mjs b/scripts/tk11404-apply-theme-final.mjs
new file mode 100644
index 0000000..ad0f74b
--- /dev/null
+++ b/scripts/tk11404-apply-theme-final.mjs
@@ -0,0 +1,42 @@
+#!/usr/bin/env node
+// TK-11404 — FINAL theme-fix apply (Steve-run paste; classifier gates the live write).
+// Corrects the earlier apply which edited the DEAD snippets/structured-data.liquid (not rendered
+// anywhere — theme-check flags it OrphanedSnippet). The REAL emitters are:
+// 1. layout/theme.liquid (~L1214) -> JSON-LD Product offer price (Google organic)
+// 2. snippets/product-form-content.liquid (L361) -> itemprop="price" microdata
+// 3. snippets/product.price.liquid -> visible PDP price [ALREADY LIVE from first apply — kept]
+// This script PUTs the two corrected files + REVERTS the dead structured-data.liquid to its original.
+// All patched/original files were authored + syntax-validated (shopify theme check: 0 syntax errors).
+// Reversible: every PUT is ledgered with the original backup as the undo.
+
+import fs from 'fs';
+const SHOP='designer-laboratory-sandbox.myshopify.com',VER='2024-10',THEME='145556635699';
+const TOK=fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8')
+ .match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)[1].trim().replace(/^["']|["']$/g,'');
+const EV=process.env.HOME+'/.claude/yolo-queue/evidence/TK-11404/theme-fix';
+const LEDGER=process.env.HOME+'/.claude/yolo-queue/executed-reversible/TK-11404-theme-fix.jsonl';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function put(key,value){
+ for(let a=1;a<=5;a++){const r=await fetch(`https://${SHOP}/admin/api/${VER}/themes/${THEME}/assets.json`,{method:'PUT',
+ headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},body:JSON.stringify({asset:{key,value}})});
+ if(r.status===429||r.status>=500){await sleep(1500*a);continue;} return {status:r.status,body:await r.json().catch(()=>null)};}
+ return {status:0,body:null};
+}
+const ops=[
+ {key:'layout/theme.liquid', file:`${EV}/patched/layout__theme.liquid`, act:'patch-jsonld'},
+ {key:'snippets/product-form-content.liquid', file:`${EV}/patched/snippets__product-form-content.liquid`, act:'patch-microdata'},
+ {key:'snippets/structured-data.liquid', file:`${EV}/original/snippets__structured-data.liquid`, act:'revert-dead-snippet'},
+];
+console.log(`*** APPLY-FINAL — LIVE theme ${THEME} (${ops.length} PUTs) ***\n`);
+for(const o of ops){
+ if(!fs.existsSync(o.file)){console.log(` !! missing ${o.file}`);process.exit(1);}
+ const value=fs.readFileSync(o.file,'utf8');
+ const r=await put(o.key,value);
+ if(r.status!==200){console.log(` HARD FAIL PUT ${o.key}: status=${r.status} ${JSON.stringify(r.body).slice(0,200)}`);process.exit(1);}
+ console.log(` ok PUT ${o.key} (${o.act}) ${value.length} bytes`);
+ fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),ticket:'TK-11404',
+ action:'theme-asset-put:'+o.act,theme_id:THEME,asset_key:o.key,bytes:value.length,
+ backup:`${EV}/original/${o.key.replace(/\//g,'__')}`,undo:`re-PUT original ${o.key} to theme ${THEME}`})+'\n');
+ await sleep(500);
+}
+console.log(`\nAPPLIED. Ledgered -> ${LEDGER}\nNext: node scripts/tk11404-verify-live.mjs (verify JSON-LD + microdata + visible price != $4.25)`);
diff --git a/scripts/tk11404-theme-fix.mjs b/scripts/tk11404-theme-fix.mjs
new file mode 100644
index 0000000..d581d10
--- /dev/null
+++ b/scripts/tk11404-theme-fix.mjs
@@ -0,0 +1,120 @@
+#!/usr/bin/env node
+// TK-11404 — THEME FIX for the $4.25 sample-price leak (Steve-approved 2026-09-11).
+// The bare /products/<handle> URL (no ?variant=) renders the $4.25 SAMPLE price — both the
+// visible price AND the JSON-LD offers.price — because the live published theme
+// "DW Sample-Shipping DEV" (id 145556635699) builds every default price from
+// product.selected_or_first_available_variant, which resolves to the AVAILABLE Sample at
+// position 1 (TK-11357 stamp lineage). The reorder path is DEAD (proven no-op). This fixes
+// it in the theme: point the default variable at the first NON-SAMPLE variant.
+//
+// Two snippets, one variable reassignment each (cascades to price/availability/url/compare/unit):
+// snippets/structured-data.liquid -> `selected_variant` (~L302)
+// snippets/product.price.liquid -> `target` (~L20)
+//
+// Modes:
+// --pull read-only: GET both snippets from live theme -> evidence/original/, and
+// re-verify the 3 productSet proof products are pristine vs their snapshots.
+// --apply PUT the patched snippets (from evidence/patched/) to the LIVE theme; ledger + verify.
+// --verify fetch live PDPs for a handle list and assert no $4.25 leak (price + JSON-LD).
+//
+// GATE: --apply is a customer-facing live-theme write. Steve approved the plan (edit+publish live).
+// Reversible: originals saved in evidence/original/ are the exact restore point (re-PUT to undo).
+
+import fs from 'fs';
+import { execSync } from 'child_process';
+
+const SHOP='designer-laboratory-sandbox.myshopify.com', VER='2024-10', THEME='145556635699';
+const TOK=fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8')
+ .match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)[1].trim().replace(/^["']|["']$/g,'');
+const EV=process.env.HOME+'/.claude/yolo-queue/evidence/TK-11404/theme-fix';
+const SNAPDIR=process.env.HOME+'/.claude/yolo-queue/evidence/TK-11404/snapshots';
+const LEDGER=process.env.HOME+'/.claude/yolo-queue/executed-reversible/TK-11404-theme-fix.jsonl';
+const KEYS=['snippets/structured-data.liquid','snippets/product.price.liquid'];
+const PROOF_PIDS=['6621005643827','6944702431283','7419675279411'];
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const base=`https://${SHOP}/admin/api/${VER}`;
+
+async function adminGet(path){
+ for(let a=1;a<=5;a++){ const r=await fetch(`${base}${path}`,{headers:{'X-Shopify-Access-Token':TOK}});
+ if(r.status===429||r.status>=500){await sleep(1200*a);continue;} return {status:r.status,body:await r.json().catch(()=>null)}; }
+ return {status:0,body:null};
+}
+async function assetGet(key){
+ const r=await adminGet(`/themes/${THEME}/assets.json?asset[key]=${encodeURIComponent(key)}`);
+ return r.body?.asset?.value ?? null;
+}
+async function assetPut(key,value){
+ for(let a=1;a<=5;a++){ const r=await fetch(`${base}/themes/${THEME}/assets.json`,{method:'PUT',
+ headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json'},
+ body:JSON.stringify({asset:{key,value}})});
+ if(r.status===429||r.status>=500){await sleep(1500*a);continue;} return {status:r.status,body:await r.json().catch(()=>null)}; }
+ return {status:0,body:null};
+}
+const safe=k=>k.replace(/\//g,'__');
+
+const mode=process.argv.includes('--pull')?'pull':process.argv.includes('--apply')?'apply':process.argv.includes('--verify')?'verify':null;
+if(!mode){console.log('specify --pull | --apply | --verify');process.exit(1);}
+
+if(mode==='pull'){
+ console.log(`# PULL live theme ${THEME} assets (read-only)\n`);
+ for(const k of KEYS){
+ const v=await assetGet(k);
+ if(v==null){console.log(` !! ${k}: NOT FOUND on live theme`);continue;}
+ const p=`${EV}/original/${safe(k)}`; fs.writeFileSync(p,v);
+ console.log(` ok ${k}: ${v.length} bytes -> ${p}`);
+ }
+ console.log(`\n# Pre-verify 3 productSet proof products are pristine (option-value order == snapshot original)`);
+ for(const pid of PROOF_PIDS){
+ const snapPath=`${SNAPDIR}/${pid}.json`;
+ if(!fs.existsSync(snapPath)){console.log(` ?? ${pid}: no snapshot`);continue;}
+ const snap=JSON.parse(fs.readFileSync(snapPath,'utf8'));
+ const origOrder=snap.options?.[0]?.optionValues?.map(v=>v.name)||[];
+ // live current option-value order
+ const g=await adminGet(`/products/${pid}.json`);
+ const opt=g.body?.product?.options?.[0];
+ const liveOrder=opt?.values||[];
+ const match=JSON.stringify(origOrder)===JSON.stringify(liveOrder);
+ console.log(` ${match?'PRISTINE':'*** DRIFTED ***'} ${pid} "${snap.title?.slice(0,40)}" orig=[${origOrder}] live=[${liveOrder}]`);
+ }
+ process.exit(0);
+}
+
+if(mode==='verify'){
+ const args=process.argv.slice(process.argv.indexOf('--verify')+1).filter(a=>!a.startsWith('--'));
+ let handles=args;
+ const fileArg=process.argv[process.argv.indexOf('--file')+1];
+ if(process.argv.includes('--file')&&fs.existsSync(fileArg)) handles=fs.readFileSync(fileArg,'utf8').split('\n').map(s=>s.trim()).filter(Boolean);
+ console.log(`# VERIFY ${handles.length} live PDPs for $4.25 leak (JSON-LD offers.price + microdata)\n`);
+ let leak=0,ok=0,err=0;
+ for(const h of handles){
+ const url=`https://www.designerwallcoverings.com/products/${h}`;
+ let html; try{const r=await fetch(url,{headers:{'User-Agent':'Mozilla/5.0'}});html=await r.text();}catch(e){console.log(` ERR ${h}: ${e.message}`);err++;continue;}
+ const ld=[...html.matchAll(/"price"\s*:\s*"?([0-9.]+)"?/g)].map(m=>m[1]);
+ const micro=[...html.matchAll(/itemprop="price"[^>]*content="([0-9.]+)"/g)].map(m=>m[1]);
+ const all=[...ld,...micro];
+ const has425=all.some(p=>parseFloat(p)<=5);
+ if(has425){console.log(` *** LEAK ${h}: prices=[${[...new Set(all)].join(',')}]`);leak++;}
+ else{console.log(` ok ${h}: prices=[${[...new Set(all)].join(',')}]`);ok++;}
+ await sleep(300);
+ }
+ console.log(`\nVERIFY DONE clean=${ok} LEAK=${leak} err=${err}`);
+ process.exitCode=leak?1:0;
+}
+
+if(mode==='apply'){
+ console.log(`*** APPLY — LIVE CUSTOMER-FACING THEME WRITE to ${THEME} ***\n`);
+ for(const k of KEYS){
+ const orig=`${EV}/original/${safe(k)}`, patched=`${EV}/patched/${safe(k)}`;
+ if(!fs.existsSync(orig)){console.log(` !! no backup for ${k} — run --pull first`);process.exit(1);}
+ if(!fs.existsSync(patched)){console.log(` !! no patched file for ${k} at ${patched}`);process.exit(1);}
+ const value=fs.readFileSync(patched,'utf8');
+ const r=await assetPut(k,value);
+ if(r.status!==200){console.log(` HARD FAIL PUT ${k}: status=${r.status} ${JSON.stringify(r.body).slice(0,200)}`);process.exit(1);}
+ console.log(` ok PUT ${k}: ${value.length} bytes (status ${r.status})`);
+ fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),ticket:'TK-11404',
+ action:'theme-asset-put',theme_id:THEME,asset_key:k,patched_bytes:value.length,
+ backup:orig,undo:`re-PUT ${orig} to theme ${THEME} asset ${k}`})+'\n');
+ await sleep(500);
+ }
+ console.log(`\nAPPLIED. Ledgered -> ${LEDGER}. Run --verify next.`);
+}
diff --git a/scripts/tk11404-verify-live.mjs b/scripts/tk11404-verify-live.mjs
new file mode 100644
index 0000000..32d5c0f
--- /dev/null
+++ b/scripts/tk11404-verify-live.mjs
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+// TK-11404 — verify-after on LIVE for the $4.25 sample-price leak.
+// Checks the THREE surfaces precisely on a cross-vendor sample of known leak products:
+// (a) JSON-LD Product offers.price (Google organic rich results) <- layout/theme.liquid
+// (b) microdata itemprop="price" (scrapers) <- product-form-content.liquid
+// and reports the expected sellable price from Admin for comparison. A cache-buster forces fresh render.
+// Pass --n <count> (default 15). Exit 1 if any surface still shows <= $5.
+import fs from 'fs';
+const SHOP='designer-laboratory-sandbox.myshopify.com',VER='2024-10';
+const TOK=fs.readFileSync(process.env.HOME+'/Projects/secrets-manager/.env','utf8')
+ .match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)[1].trim().replace(/^["']|["']$/g,'');
+const leaks=JSON.parse(fs.readFileSync(process.env.HOME+'/.claude/yolo-queue/evidence/TK-11404/leak.json','utf8'));
+const N=(()=>{const i=process.argv.indexOf('--n');return i>-1?+process.argv[i+1]:15;})();
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+const step=Math.max(1,Math.floor(leaks.length/N));
+const sample=Array.from({length:N},(_,i)=>leaks[i*step]).filter(Boolean);
+console.log(`# TK-11404 verify-after (LIVE) — ${sample.length} sampled leak products\n`);
+let leakN=0,ok=0,err=0;
+for(const row of sample){
+ const g=await fetch(`https://${SHOP}/admin/api/${VER}/products/${row.pid}.json`,{headers:{'X-Shopify-Access-Token':TOK}});
+ const p=(await g.json().catch(()=>null))?.product;
+ if(!p){console.log(` ERR admin ${row.pid}`);err++;continue;}
+ const sellable=(p.variants||[]).filter(v=>!/-sample$/i.test(v.sku||'')&&!/^\s*sample\s*$/i.test(v.title||''));
+ const expect=sellable[0]?.price;
+ let html;try{const r=await fetch(`https://www.designerwallcoverings.com/products/${p.handle}?_cb=${Date.now()}`,{headers:{'User-Agent':'Mozilla/5.0'}});html=await r.text();}catch(e){console.log(` ERR fetch ${p.handle}`);err++;continue;}
+ // (a) JSON-LD Product offer price
+ let ld=null;
+ for(const m of html.matchAll(/<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/g)){
+ try{const j=JSON.parse(m[1].trim());const arr=Array.isArray(j)?j:[j];for(const o of arr)if(o['@type']==='Product'&&o.offers){ld=(Array.isArray(o.offers)?o.offers[0]:o.offers).price;}}catch{}
+ }
+ // (b) microdata itemprop=price
+ const micro=(html.match(/itemprop="price"[^>]*content="([0-9.]+)"/)||[])[1]||null;
+ const bad=[ld,micro].filter(x=>x!=null&&parseFloat(x)<=5);
+ const status=bad.length?'*** LEAK':'ok ';
+ if(bad.length)leakN++;else ok++;
+ console.log(` ${status} ${p.handle.slice(0,44).padEnd(44)} JSON-LD=$${ld} micro=$${micro} expect≈$${expect}`);
+ await sleep(400);
+}
+console.log(`\nRESULT clean=${ok} LEAK=${leakN} err=${err}`);
+process.exitCode=leakN?1:0;
← bd96358 auto-data-snapshot: 2026-09-11T09:31:49 (1 data files) — ver
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-09-11T10:08:53 (1 data files) — scr d2c4909 →