← back to Tk10656 Empty Collections
scan-collections.mjs
33 lines
// TK-10656 — READ-ONLY scan of ALL collections: capture handle/title/productsCount +
// whether published to the Online Store. Flags the customer-facing dead-page class:
// published to Online Store AND productsCount == 0 (a live collection page showing shoppers 0 products).
import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';
const txt=readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`,'utf8');
const env={}; for(const l of txt.split('\n')){const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m) env[m[1]]=m[2].replace(/^["']|["']$/g,'');}
const STORE=env.SHOPIFY_STORE_DOMAIN, TOKEN=env.SHOPIFY_FULL_ACCESS_TOKEN, API='2024-10';
const ONLINE='gid://shopify/Publication/22208643184';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v={}){for(let a=0;a<10;a++){try{const res=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'Content-Type':'application/json','X-Shopify-Access-Token':TOKEN},body:JSON.stringify({query:q,variables:v})});const tx=await res.text();let j;try{j=JSON.parse(tx);}catch{await sleep(1500*(a+1));continue;}if(j.errors){if(/THROTTLED/i.test(JSON.stringify(j.errors))){await sleep(1500*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}catch(e){if(a<9){await sleep(1500*(a+1));continue;}throw e;}}}
const DUMP='data/collections-dump.jsonl'; writeFileSync(DUMP,'');
let cursor=null, n=0, publishedEmpty=0, unpublishedEmpty=0, publishedNonEmpty=0;
const emptyPublished=[];
while(true){
const d=await gql(`query($c:String){ collections(first:100, after:$c){ pageInfo{ hasNextPage endCursor }
nodes{ id handle title productsCount{count} publishedOnPublication(publicationId:"${ONLINE}") } } }`,{c:cursor});
const buf=[];
for(const c of d.collections.nodes){ n++;
const rec={handle:c.handle,title:c.title,count:c.productsCount?.count??null,pub:c.publishedOnPublication};
buf.push(JSON.stringify(rec));
if(rec.count===0){ if(rec.pub){ publishedEmpty++; if(emptyPublished.length<500) emptyPublished.push(rec); } else unpublishedEmpty++; }
else if(rec.pub) publishedNonEmpty++;
}
appendFileSync(DUMP, buf.join('\n')+'\n');
if(n%500===0) process.stderr.write(` scanned ${n} collections, published-empty ${publishedEmpty}\n`);
if(!d.collections.pageInfo.hasNextPage) break;
cursor=d.collections.pageInfo.endCursor;
}
writeFileSync('data/empty-published-collections.json', JSON.stringify(emptyPublished,null,2));
console.log(JSON.stringify({total_collections:n, published_empty_ZERO_products:publishedEmpty,
unpublished_empty:unpublishedEmpty, published_nonempty:publishedNonEmpty,
sample:emptyPublished.slice(0,15)},null,2));