← back to Dw Yolo Loop

scripts/jsonld-audit/jsonld-audit.mjs

89 lines

// jsonld-audit — READ-ONLY, $0. Samples live PDPs, parses the Product schema.org
// JSON-LD, validates presence + well-formedness + offers(price/currency/availability),
// and — the high-value check (DTD/officer flagged) — cross-references the JSON-LD
// offer price against the product's ACTUAL variant prices to catch the $4.25
// SAMPLE-TRAP leaking into structured data (which would be a Google rich-result +
// Merchant-Center disapproval risk). All live; no writes.
const WWW='https://www.designerwallcoverings.com';
const UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function get(u){for(let a=0;a<4;a++){try{const r=await fetch(u,{headers:{'User-Agent':UA}});if(r.status===429){await sleep(1500*(a+1));continue;}return {status:r.status,text:await r.text()};}catch(e){await sleep(800*(a+1));}}return {status:0,text:''};}

// 1. gather live handles from several product sitemaps (vendor/age diversity)
const smIdx=await get(`${WWW}/sitemap.xml`);
const prodMaps=[...smIdx.text.matchAll(/<loc>([^<]+sitemap_products_\d+\.xml[^<]*)<\/loc>/g)].map(m=>m[1].replace(/&amp;/g,'&')).filter(u=>!/\/en-ca\//.test(u));
// take a spread: first, middle, last few maps
const pick=[prodMaps[0],prodMaps[Math.floor(prodMaps.length/3)],prodMaps[Math.floor(2*prodMaps.length/3)],prodMaps[prodMaps.length-1]].filter(Boolean);
let handles=[];
for(const sm of pick){ const x=await get(sm); const hs=[...x.text.matchAll(/<loc>[^<]*\/products\/([^<\/?]+)/g)].map(m=>m[1]); handles.push(...hs.slice(0,15)); await sleep(150); }
handles=[...new Set(handles)].slice(0,50);
console.log(`=== jsonld-audit (READ-ONLY, $0) ===\nsampling ${handles.length} live PDPs\n`);

function parseProduct(html){
  const blocks=[...html.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map(m=>m[1]);
  let parseFail=false, product=null;
  for(const b of blocks){
    let j; try{ j=JSON.parse(b); }catch{ parseFail=true; continue; }
    const cands=j['@graph']?j['@graph']:[j];
    for(const c of cands){ if(c['@type']==='Product') product=c; }
  }
  return {blockCount:blocks.length, parseFail, product};
}

const rows=[];
let done=0;
for(const h of handles){
  const pdp=await get(`${WWW}/products/${h}`);
  if(pdp.status!==200){ rows.push({h,err:'pdp '+pdp.status}); done++; continue; }
  const {blockCount,parseFail,product}=parseProduct(pdp.text);
  // actual variant prices
  const pj=await get(`${WWW}/products/${h}.json`);
  let variantPrices=[]; try{ variantPrices=(JSON.parse(pj.text).product?.variants||[]).map(v=>parseFloat(v.price)); }catch{}
  const minV=variantPrices.length?Math.min(...variantPrices):null;
  const maxV=variantPrices.length?Math.max(...variantPrices):null;
  const off=product?.offers;
  const offer=Array.isArray(off)?off[0]:off;
  const ldPrice=offer?parseFloat(offer.price):null;
  const row={
    h,
    hasProduct: !!product,
    parseFail,
    hasOffer: !!offer,
    ldPrice,
    currency: offer?.priceCurrency||null,
    availability: (offer?.availability||'').replace(/https?:\/\/schema\.org\//,''),
    minV, maxV,
    sampleTrap: (ldPrice!==null && ldPrice<=4.25 && maxV!==null && maxV>4.25), // JSON-LD shows sample price while a real variant exists
    priceMismatch: (ldPrice!==null && minV!==null && maxV!==null && (ldPrice<minV-0.01 || ldPrice>maxV+0.01)),
  };
  rows.push(row);
  done++;
  if(done%10===0) console.log(`  ${done}/${handles.length}`);
  await sleep(180);
}

const ok=rows.filter(r=>r.hasProduct);
const noProduct=rows.filter(r=>!r.hasProduct && !r.err);
const errs=rows.filter(r=>r.err);
const parseFails=rows.filter(r=>r.parseFail);
const noOffer=ok.filter(r=>!r.hasOffer);
const badCurrency=ok.filter(r=>r.hasOffer && r.currency!=='USD');
const badAvail=ok.filter(r=>r.hasOffer && !['InStock','OutOfStock','PreOrder','BackOrder','Discontinued'].includes(r.availability));
const sampleTraps=rows.filter(r=>r.sampleTrap);
const mismatches=rows.filter(r=>r.priceMismatch && !r.sampleTrap);

console.log(`\n=== RESULTS (${rows.length} sampled) ===`);
console.log(`Product JSON-LD present: ${ok.length} | missing: ${noProduct.length} | pdp-fetch-err: ${errs.length}`);
console.log(`JSON parse failures: ${parseFails.length}`);
console.log(`has offers: ${ok.length-noOffer.length} | no offers block: ${noOffer.length}`);
console.log(`currency != USD: ${badCurrency.length}`);
console.log(`invalid availability value: ${badAvail.length}`);
console.log(`\n*** $4.25 SAMPLE-TRAP in JSON-LD (ld price<=4.25 but real variant exists): ${sampleTraps.length} ***`);
console.log(`other price mismatch (ld price outside variant range): ${mismatches.length}`);
if(sampleTraps.length) sampleTraps.slice(0,15).forEach(r=>console.log(`  TRAP - ${r.h}: ld=$${r.ldPrice} vs variants $${r.minV}-$${r.maxV}`));
if(mismatches.length) mismatches.slice(0,10).forEach(r=>console.log(`  MISMATCH - ${r.h}: ld=$${r.ldPrice} vs $${r.minV}-$${r.maxV}`));

import fs from 'fs';
fs.writeFileSync('/tmp/jsonld-audit.json',JSON.stringify({ts:new Date().toISOString(),sampled:rows.length,summary:{product:ok.length,missing:noProduct.length,errs:errs.length,parseFails:parseFails.length,noOffer:noOffer.length,badCurrency:badCurrency.length,badAvail:badAvail.length,sampleTraps:sampleTraps.length,mismatches:mismatches.length},rows},null,2));
console.log('\nwrote /tmp/jsonld-audit.json');