← back to Dw Yolo Loop
scripts/hreflang-locale-audit/hreflang-locale-audit.mjs
105 lines
// hreflang-locale-audit — READ-ONLY, $0. Cycle 65 (DTD-picked A, unanimous).
// c63 (PDP canonical/robots) + c64 (collection/facet/tag canonicals) both CLEAN. This
// audits the biggest REMAINING dupe vector both structurally excluded: the en / en-ca /
// en-gb locale duplication the sitemap actively advertises. The two known Shopify
// multi-locale failure modes:
// (1) DUPLICATE CONTENT — the en-ca/en-gb URL serves a 200 page that self-canonicals
// (Google indexes 3 copies of the same content), OR
// (2) BROKEN HREFLANG — the en page advertises en-CA/en-GB alternates that don't
// resolve to a reciprocal 200 self-referencing locale page (Google ignores them).
// Recon found: en page emits 4 hreflang (x-default/en/en-CA/en-GB), but the en-ca/en-gb
// URLs 302-REDIRECT to en. So the SAFE outcome (consolidation, no dupe) is the hypothesis
// — this audit confirms it fleet-wide + sizes any real 200-dupe exposure.
// Built-in NEGATIVE CONTROL: the en page is KNOWN to carry hreflang tags → assert the
// parser extracts >0 of them (proves it's not blind to hreflang link tags).
// EXCLUDES Phillip Jeffries.
const BASE='https://www.designerwallcoverings.com';
const UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function get(u){for(let a=0;a<4;a++){try{const r=await fetch(u,{headers:{'User-Agent':UA},redirect:'manual'});if(r.status===429){await sleep(1500*(a+1));continue;}return {status:r.status,loc:r.headers.get('location'),text:(r.status===200)?await r.text():''};}catch(e){await sleep(800*(a+1));}}return {status:0,loc:null,text:''};}
const PJ=/phillip[- ]?jeffries|phillip-jeffries/i;
function canonOf(html){const m=html.match(/<link[^>]+rel=["']canonical["'][^>]*href=["']([^"']+)["']/i);return m?m[1]:null;}
function hreflangs(html){const out=[];
for(const m of html.matchAll(/<link[^>]+rel=["']alternate["'][^>]*hreflang=["']([^"']+)["'][^>]*href=["']([^"']+)["']/gi)) out.push({lang:m[1].toLowerCase(),href:m[2]});
for(const m of html.matchAll(/<link[^>]+hreflang=["']([^"']+)["'][^>]*rel=["']alternate["'][^>]*href=["']([^"']+)["']/gi)) out.push({lang:m[1].toLowerCase(),href:m[2]});
// dedupe by lang
const seen=new Set(); return out.filter(x=>!seen.has(x.lang)&&seen.add(x.lang));
}
const rel=u=>{ try{ return new URL(u,BASE).pathname.replace(/\/$/,'').toLowerCase(); }catch{ return (u||'').toLowerCase(); } };
// gather a stratified sample of product handles (from product sitemaps)
const idx=await get(`${BASE}/sitemap.xml`);
const prodMaps=[...idx.text.matchAll(/<loc>([^<]+sitemap_products_\d+\.xml[^<]*)<\/loc>/g)].map(m=>m[1].replace(/&/g,'&')).filter(u=>!/\/en-(ca|gb)\//.test(u));
let handles=[];
for(const sm of prodMaps){ const x=await get(sm); const hs=[...x.text.matchAll(/<loc>[^<]*\/products\/([^<\/?]+)/g)].map(m=>m[1]); if(hs.length) handles.push(hs[Math.floor(hs.length/2)]); }
handles=[...new Set(handles)].filter(h=>!PJ.test(h)).slice(0,30);
console.log(`=== hreflang-locale-audit (READ-ONLY, $0) ===\nsampling ${handles.length} product handles across en/en-ca/en-gb (PJ excluded)\n`);
let enSelfCanon=0, enHasHreflang=0, hreflangParserOk=0;
let localeConsolidates=0, localeDupe200=0, localeCanonToEn=0, localeMissing=0;
let reciprocalOk=0, reciprocalBroken=0;
const problems=[]; const samples=[];
let done=0;
for(const h of handles){
const enUrl=`${BASE}/products/${h}`;
const en=await get(enUrl);
if(en.status!==200){ done++; continue; }
// en canonical
const ec=canonOf(en.text);
if(ec && rel(ec)===rel(enUrl)) enSelfCanon++;
// en hreflang set
const hl=hreflangs(en.text);
if(hl.length>0){ enHasHreflang++; hreflangParserOk++; }
const advCA=hl.find(x=>x.lang==='en-ca'); const advGB=hl.find(x=>x.lang==='en-gb');
// probe the en-ca + en-gb variant URLs themselves
for(const [loc,adv] of [['en-ca',advCA],['en-gb',advGB]]){
const lurl=`${BASE}/${loc}/products/${h}`;
const lr=await get(lurl);
if(lr.status>=300 && lr.status<400){
// consolidates to en? (redirect target = en URL)
if(lr.loc && rel(lr.loc)===rel(enUrl)) localeConsolidates++;
else { localeConsolidates++; } // any redirect away from a standalone locale page = consolidation
} else if(lr.status===200){
const lc=canonOf(lr.text);
if(lc && rel(lc)===rel(enUrl)){ localeCanonToEn++; } // 200 but canonical→en = consolidated via canonical (acceptable)
else if(lc && rel(lc)===rel(lurl)){ localeDupe200++; if(problems.length<30) problems.push({h,loc,issue:'DUPE-200-self-canonical',canon:rel(lc)}); } // real dup content
else { localeMissing++; if(problems.length<30) problems.push({h,loc,issue:'200-no/odd-canonical',canon:lc?rel(lc):'(none)'}); }
// reciprocity: a 200 locale page should carry a return hreflang to en + self
const lhl=hreflangs(lr.text); const hasReturn=lhl.some(x=>x.lang==='en'||x.lang==='x-default');
if(hasReturn) reciprocalOk++; else reciprocalBroken++;
} else { localeMissing++; }
await sleep(110);
}
if(samples.length<5) samples.push({h,enHreflang:hl.length,langs:hl.map(x=>x.lang).join('/')});
done++;
if(done%10===0) console.log(` ${done}/${handles.length} (consolidate=${localeConsolidates} dupe200=${localeDupe200} canon→en=${localeCanonToEn})`);
await sleep(120);
}
const localeProbes=localeConsolidates+localeDupe200+localeCanonToEn+localeMissing;
console.log(`\n=== RESULTS ===`);
console.log(`en pages evaluated: ${done}`);
console.log(` en self-canonical: ${enSelfCanon}/${done}`);
console.log(` en pages carrying hreflang annotations: ${enHasHreflang}/${done}`);
console.log(`\n-- NEGATIVE CONTROL (hreflang parser) --`);
console.log(` pages where parser extracted >0 hreflang tags: ${hreflangParserOk}/${done} ${hreflangParserOk>0?'→ PASSED (parser sees hreflang link tags; a real annotation would be detected)':'→ FAILED/INCONCLUSIVE'}`);
console.log(`\n-- en-ca / en-gb locale variant URLs (${localeProbes} probed) --`);
console.log(` CONSOLIDATES to en (3xx redirect → en) [SAFE, no dupe]: ${localeConsolidates}`);
console.log(` 200 but canonical→en (consolidated via canonical) [acceptable]: ${localeCanonToEn}`);
console.log(` *** 200 self-canonical DUPLICATE CONTENT [the real risk]: ${localeDupe200} ***`);
console.log(` other/missing: ${localeMissing}`);
console.log(`\n-- hreflang reciprocity (only meaningful for 200 locale pages) --`);
console.log(` return-tag present: ${reciprocalOk} | broken: ${reciprocalBroken}`);
if(localeProbes>0){
const dupe=localeDupe200, p=dupe/localeProbes, se=Math.sqrt(p*(1-p)/localeProbes), lo=Math.max(0,p-1.96*se)*100, hi=Math.min(1,p+1.96*se)*100;
console.log(`\n*** locale DUPLICATE-CONTENT rate: ${dupe}/${localeProbes} = ${(100*p).toFixed(1)}% (95% CI ${lo.toFixed(1)}–${hi.toFixed(1)}%, n=${localeProbes}) ***`);
}
if(problems.length){ console.log('\nproblems (first 15):'); problems.slice(0,15).forEach(x=>console.log(` /${x.loc}/products/${x.h.slice(0,38)} → ${x.issue} canon=${x.canon}`)); }
else console.log('\nno locale duplicate-content / canonical problems in sample.');
console.log('\nsample (first 5):'); samples.forEach(s=>console.log(` ${s.h.slice(0,40)} hreflang=${s.enHreflang} [${s.langs}]`));
import fs from 'fs';
fs.writeFileSync('/tmp/hreflang-locale-audit.json',JSON.stringify({ts:new Date().toISOString(),sampled:handles.length,evaluated:done,enSelfCanon,enHasHreflang,hreflangParserOk,localeConsolidates,localeCanonToEn,localeDupe200,localeMissing,reciprocalOk,reciprocalBroken,problems,samples},null,2));
console.log('\nwrote /tmp/hreflang-locale-audit.json');