[object Object]

← 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

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 →