← back to Dw Yolo Loop

scripts/orphan-collections/orphan-collection-scan.mjs

39 lines

// orphan-collection-scan (c39) — READ-ONLY. Pulls every live Shopify collection
// from /collections.json (public feed, includes products_count) and flags
// EMPTY (0 products) + NEAR-EMPTY (<MIN) ones = dead storefront nav links +
// wasted SEO/internal-link equity + a shopper clicking into nothing. Live read,
// so it sidesteps the distrusted mirror (c37/c23). $0.
import fs from 'node:fs';
const MIN = parseInt(process.argv[2]||'3',10);
const BASE='https://www.designerwallcoverings.com';
const OUT=`${process.env.HOME}/.claude/yolo-queue/orphan-collections-2026-06-16.json`;
const MD =`${process.env.HOME}/.claude/yolo-queue/orphan-collections-2026-06-16.md`;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function page(p){
  for(let a=0;a<4;a++){
    try{ const r=await fetch(`${BASE}/collections.json?limit=250&page=${p}`,{signal:AbortSignal.timeout(15000)});
      if(r.status===200) return (await r.json()).collections||[];
      if(r.status===429){await sleep(2000*(a+1));continue;} return null; }
    catch(e){ await sleep(1500*(a+1)); }
  }
  return null;
}
const all=[];
for(let p=1;p<=20;p++){ const c=await page(p); if(c===null){console.error('page',p,'failed');break;} if(!c.length)break; all.push(...c); await sleep(200); }
const empty=all.filter(c=>(c.products_count??0)===0);
const near=all.filter(c=>(c.products_count??0)>0 && (c.products_count??0)<MIN);
const report={generated_at:new Date().toISOString(), total_collections:all.length, empty_count:empty.length, near_empty_count:near.length, min:MIN,
  empty:empty.map(c=>({handle:c.handle,title:c.title,published_at:c.published_at})),
  near_empty:near.map(c=>({handle:c.handle,title:c.title,products_count:c.products_count}))};
fs.writeFileSync(OUT,JSON.stringify(report,null,2));
let md=`# Orphan-collection scan (live) — ${new Date().toISOString().slice(0,16)}\n\n`;
md+=`**READ-ONLY (live /collections.json), \$0.** ${all.length} live collections scanned. Empty = 0 products (dead nav + wasted SEO). Near-empty = <${MIN}.\n\n`;
md+=`## ${empty.length} EMPTY collections (0 live products)\n`;
if(empty.length){ md+=`| handle | title |\n|---|---|\n`; for(const c of empty.slice(0,80)) md+=`| ${c.handle} | ${(c.title||'').slice(0,50)} |\n`; if(empty.length>80) md+=`| …+${empty.length-80} more | |\n`; } else md+=`None — every collection has at least 1 product. 🟢\n`;
md+=`\n## ${near.length} NEAR-EMPTY (<${MIN} products)\n`;
if(near.length){ md+=`| handle | products |\n|---|--:|\n`; for(const c of near.slice(0,40)) md+=`| ${c.handle} | ${c.products_count} |\n`; }
fs.writeFileSync(MD,md);
console.log(`[orphan-collections] total=${all.length} empty=${empty.length} near_empty(<${MIN})=${near.length}`);
if(empty.length) console.log('  sample empties: '+empty.slice(0,8).map(c=>c.handle).join(', '));
console.log(`Report: ${MD}`);