← back to Tk10661 Product Seo

seo-sampler.mjs

49 lines

// TK-10661 — READ-ONLY product-page SEO sampler. Fetch a mix of regular + sample product
// pages; extract canonical + JSON-LD Product offer (price/availability/currency). Flag:
//  - canonical missing or not self-referencing
//  - JSON-LD Product missing
//  - SAMPLE product advertising $4.25 as its schema offer price (leak in structured data)
//  - availability=InStock while price looks like a $4.25 sample
const BASE='https://www.designerwallcoverings.com';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function get(u){for(let a=0;a<3;a++){try{const r=await fetch(u,{headers:{'User-Agent':'Mozilla/5.0 dw-audit'}});if(r.status>=500||r.status===429){await sleep(900*(a+1));continue;}return r;}catch(e){await sleep(700*(a+1));}}return null;}
// gather product URLs from sitemap (default locale), split regular vs sample
const idx=await (await get(`${BASE}/sitemap.xml`)).text();
const subs=[...idx.matchAll(/<loc>([^<]*sitemap_products_[^<]*)<\/loc>/g)].map(m=>m[1].replace(/&amp;/g,'&')).filter(u=>!/\/en-(ca|gb)\//.test(u));
const pick=[subs[2],subs[20],subs[40],subs[60],subs[80]].filter(Boolean);
let urls=[]; for(const s of pick){ const xml=await (await get(s)).text(); urls.push(...[...xml.matchAll(/<loc>([^<]*\/products\/[^<]+)<\/loc>/g)].map(m=>m[1])); }
urls=[...new Set(urls)];
const isSample=u=>/-sample-|memo-sample/i.test(u);
const samples=urls.filter(isSample), regular=urls.filter(u=>!isSample(u));
const shuf=a=>{for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;};
const testSet=[...shuf(regular).slice(0,25), ...shuf(samples).slice(0,25)];
function extract(html,url){
  const canon=(html.match(/<link[^>]*rel="canonical"[^>]*>/i)||[''])[0].match(/href="([^"]+)"/i)?.[1]||null;
  const blocks=[...html.matchAll(/<script[^>]*application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)].map(m=>m[1]);
  let product=null;
  for(const b of blocks){ try{ const j=JSON.parse(b.trim());
    const arr=Array.isArray(j)?j:(j['@graph']||[j]);
    for(const n of arr){ if(n && /product/i.test(n['@type']||'')) product=n; } }catch{} }
  const offer=product?.offers ? (Array.isArray(product.offers)?product.offers[0]:product.offers) : null;
  return { canon, self:canon===url, hasProduct:!!product,
    price: offer?.price ?? null, avail:(offer?.availability||'').replace('http://schema.org/','').replace('https://schema.org/',''),
    cur: offer?.priceCurrency ?? null };
}
const rows=[]; let flagSampleLeak=0, flagNoCanon=0, flagNotSelf=0, flagNoProduct=0;
for(const u of testSet){ const r=await get(u); if(!r||r.status!==200){ rows.push({url:u.replace(BASE,''),http:r?.status||0}); continue; }
  const html=await r.text(); const e=extract(html,u); const samp=isSample(u);
  const priceNum=e.price!=null?Number(e.price):null;
  const sampleLeak = samp && priceNum!=null && priceNum<=5;   // $4.25-ish advertised as offer
  if(sampleLeak) flagSampleLeak++;
  if(!e.canon) flagNoCanon++; else if(!e.self) flagNotSelf++;
  if(!e.hasProduct) flagNoProduct++;
  rows.push({url:u.replace(BASE,''),sample:samp,canon_self:e.self,hasProduct:e.hasProduct,price:e.price,avail:e.avail,cur:e.cur,sampleLeak});
  await sleep(80);
}
const fs=await import('node:fs'); fs.writeFileSync('data/seo-sample.json',JSON.stringify(rows,null,2));
const regRows=rows.filter(r=>r.sample===false&&r.http!==0), sampRows=rows.filter(r=>r.sample===true&&r.http!==0);
console.log(JSON.stringify({tested:rows.length, regular:regRows.length, sample:sampRows.length,
  flags:{no_canonical:flagNoCanon, canonical_not_selfref:flagNotSelf, no_product_schema:flagNoProduct, SAMPLE_price_leak_in_schema:flagSampleLeak},
  sample_price_examples: sampRows.filter(r=>r.sampleLeak).slice(0,6).map(r=>({url:r.url,price:r.price,avail:r.avail})),
  regular_ok_example: regRows.slice(0,2)},null,2));