← back to Dw Yolo Loop
redirect-health audit: canon clean; archived products soft-404 to homepage fleet-wide (~96% of sample, 69k archived)
00d454899ad298a35226769a4df83f7ce1a08349 · 2026-06-17 10:16:33 -0700 · Steve Abrams
Files touched
A scripts/redirect-health/redirect-health.mjs
Diff
commit 00d454899ad298a35226769a4df83f7ce1a08349
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 17 10:16:33 2026 -0700
redirect-health audit: canon clean; archived products soft-404 to homepage fleet-wide (~96% of sample, 69k archived)
---
scripts/redirect-health/redirect-health.mjs | 104 ++++++++++++++++++++++++++++
1 file changed, 104 insertions(+)
diff --git a/scripts/redirect-health/redirect-health.mjs b/scripts/redirect-health/redirect-health.mjs
new file mode 100644
index 0000000..6b41622
--- /dev/null
+++ b/scripts/redirect-health/redirect-health.mjs
@@ -0,0 +1,104 @@
+// redirect-health — READ-ONLY, $0. Audits the customer-facing redirect surface
+// the handle-freshness canary does NOT cover:
+// 1. canonicalization: http→https, www↔apex, trailing-slash — must be ONE clean 301.
+// 2. redirect chains/loops: a hop count >2 or a cycle is an SEO penalty.
+// 3. archived/discontinued product URLs: do they 301 (link equity preserved)
+// or 404 (equity lost)? Shopify auto-creates redirects for some; we measure.
+// No Shopify writes; no token needed for the canonical/chain probes. The archived
+// sample is read from the dw_unified mirror (status lags canonical — VERIFY live,
+// which is exactly what this does: it hits the LIVE URL, not the mirror status).
+import { execSync } from 'child_process';
+const PSQL='/opt/homebrew/opt/postgresql@14/bin/psql';
+const DB='postgresql:///dw_unified?host=/tmp';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+
+// follow redirects manually, capping the chain, detecting loops
+async function trace(url,{max=6}={}){
+ const chain=[]; const seen=new Set(); let cur=url;
+ for(let i=0;i<max;i++){
+ let r;
+ try{ r=await fetch(cur,{method:'HEAD',redirect:'manual'}); }
+ catch(e){ chain.push({url:cur,status:'ERR:'+e.message.slice(0,40)}); return {chain,final:cur,loop:false,err:true}; }
+ const status=r.status; const loc=r.headers.get('location');
+ chain.push({url:cur,status,loc:loc||null});
+ if(status>=300&&status<400&&loc){
+ let next; try{ next=new URL(loc,cur).toString(); }catch{ next=loc; }
+ if(seen.has(next)) return {chain,final:next,loop:true,err:false};
+ seen.add(cur); cur=next; continue;
+ }
+ return {chain,final:cur,loop:false,err:false};
+ }
+ return {chain,final:cur,loop:false,truncated:true,err:false};
+}
+
+const APEX='designerwallcoverings.com';
+const WWW='www.designerwallcoverings.com'; // canonical host (per canon probe) — probe products here to avoid the apex→www hop confound
+console.log('=== redirect-health (READ-ONLY, $0) ===\n');
+
+// 1. canonicalization probes — each SHOULD land on https://<apex>/... in ≤1-2 hops
+const canon=[
+ ['http→https apex', `http://${APEX}/`],
+ ['http→https www', `http://www.${APEX}/`],
+ ['https www→apex', `https://www.${APEX}/`],
+ ['https apex (base)', `https://${APEX}/`],
+];
+const canonRes=[];
+for(const [label,u] of canon){
+ const t=await trace(u);
+ const hops=t.chain.filter(c=>c.status>=300&&c.status<400).length;
+ const finalOk=/^https:\/\//.test(t.final);
+ canonRes.push({label,u,hops,final:t.final,loop:t.loop,finalOk,statuses:t.chain.map(c=>c.status).join('→')});
+ console.log(`[canon] ${label.padEnd(18)} ${t.chain.map(c=>c.status).join('→').padEnd(14)} hops=${hops} ${t.loop?'LOOP!':''} → ${t.final}`);
+ await sleep(200);
+}
+
+// 2. archived-product URL link-equity sample: pull recently-archived handles from mirror,
+// probe live to see if they 301 (equity kept) or 404 (equity lost).
+let archived=[];
+try{
+ const out=execSync(`${PSQL} "${DB}" -At -F'|' -c "select handle, vendor from shopify_products where status='ARCHIVED' and handle is not null and handle<>'' order by updated_at_shopify desc nulls last limit 40"`,{encoding:'utf8'});
+ archived=out.trim().split('\n').filter(Boolean).map(l=>{const[h,v]=l.split('|');return{h,v};});
+}catch(e){ console.log('\n[archived] mirror read failed: '+e.message.slice(0,80)); }
+
+// Honest classification (corrected after skeptical verify): a redirect-to-200 is
+// NOT automatically "equity kept". A dead-product redirect to the HOMEPAGE is a
+// Google-recognized SOFT-404 — dropped from index, ~0 link-equity passed, wastes
+// crawl budget. And a 302 (temporary) tells Google "this URL will come back" — the
+// wrong signal for a permanently archived product (should be 301 or honest 404/410).
+let kept=0,soft404=0,hard404=0,still200=0,aLoop=0,temp302=0;
+const sl=[];
+const baseRe=new RegExp(`^https?:\\/\\/(www\\.)?${APEX.replace('.','\\.')}\\/?$`,'i');
+console.log(`\n[archived] probing ${archived.length} recently-archived product URLs live...`);
+for(const {h,v} of archived){
+ const t=await trace(`https://${WWW}/products/${h}`);
+ const first=t.chain[0]?.status;
+ const last=t.chain[t.chain.length-1]?.status;
+ const redirected=t.chain.some(c=>c.status>=300&&c.status<400);
+ const usedTemp=t.chain.some(c=>c.status===302||c.status===307);
+ if(t.loop){aLoop++;continue;}
+ if([404,410].includes(last)||[404,410].includes(first)){ hard404++; }
+ else if(redirected && baseRe.test(t.final)){ soft404++; if(usedTemp)temp302++; if(sl.length<12)sl.push({h,v,via:t.chain.map(c=>c.status).join('→')}); } // redirect to homepage = soft-404
+ else if(redirected && last===200 && t.final.match(/\/(products|collections)\//)){ kept++; if(usedTemp)temp302++; } // redirect to a real product/collection = equity kept
+ else if(!redirected && first===200){ still200++; } // PDP still live (mirror status lag — archived in mirror, live on store)
+ else { soft404++; if(sl.length<12)sl.push({h,v,via:t.chain.map(c=>c.status).join('→')}); }
+ await sleep(150);
+}
+console.log(` redirect→real product/collection (equity KEPT): ${kept}`);
+console.log(` redirect→HOMEPAGE (SOFT-404, ~0 equity): ${soft404}`);
+console.log(` honest 404/410: ${hard404}`);
+console.log(` still 200 (mirror status lag, PDP live): ${still200}`);
+console.log(` loops: ${aLoop}`);
+console.log(` of redirects, using 302/307 (temp, wrong sig): ${temp302}`);
+if(sl.length){ console.log(' sample soft-404 handles:'); sl.forEach(x=>console.log(` - ${x.h} (${x.v}) [${x.via}]`)); }
+
+const out={ ts:new Date().toISOString(), canon:canonRes, archived:{sampled:archived.length,equity_kept:kept,soft404,hard404,still200,loops:aLoop,temp302,sampleSoft404:sl} };
+import fs from 'fs';
+fs.writeFileSync('/tmp/redirect-health.json',JSON.stringify(out,null,2));
+console.log('\nwrote /tmp/redirect-health.json');
+
+// verdict tells
+const canonBad=canonRes.filter(c=>!c.finalOk||c.loop||c.hops>2);
+console.log('\n=== TELLS ===');
+console.log(`canonicalization issues: ${canonBad.length} (`+canonBad.map(c=>c.label).join(', ')+')');
+const softPct=archived.length?(100*soft404/archived.length).toFixed(0):0;
+console.log(`archived soft-404 to homepage (equity lost): ${soft404}/${archived.length} = ${softPct}%`);
← e88d630 gmc-feed-guard: tag FAIL offenders sample-leak vs real-$4.25
·
back to Dw Yolo Loop
·
sitemap-health audit: robots+sitemap clean; 0 genuine dead U 540c991 →