← back to Designer Wallcoverings
pending-approval/tile-images-fix-2026-08-07/apply-fix.mjs
89 lines
#!/usr/bin/env node
// Patch dead tile images on the 4 DW category Pages via Admin Pages API.
// Requires a token with write_content/read_content (paste as env var).
// DRY-RUN by default. Add --apply to write.
//
// SHOPIFY_CONTENT_TOKEN=shpat_xxx node apply-fix.mjs # preview
// SHOPIFY_CONTENT_TOKEN=shpat_xxx node apply-fix.mjs --apply # write
import fs from 'fs';
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const API = `https://${SHOP}/admin/api/2024-10`;
const TOKEN = process.env.SHOPIFY_CONTENT_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
const APPLY = process.argv.includes('--apply');
const DO_MIS = process.argv.includes('--mislabels');
const HANDLES = ['shop-by-material','brands','collections','designer-wallpaper-styles'];
// Verified-live repoints, tile-scoped by img alt so a shared old-link on another tile is untouched.
// Lincrusta -> embossed is PROPOSED (off by default); enable with --lincrusta.
const MISLABELS = {
'shop-by-material': [
{ label:'Mica', from:'/collections/shiny-mylar-mirror', to:'/collections/mica' },
{ label:'Jute', from:'/collections/madagascar-cloth', to:'/collections/jute' },
{ label:'Linen', from:'/collections/faux-silk', to:'/collections/linen' },
],
'designer-wallpaper-styles': [
{ label:'Mica', from:'/collections/art-deco-metallics-1', to:'/collections/mica' },
...(process.argv.includes('--lincrusta')
? [{ label:'Lincrusta', from:'/collections/japonisme-wallpaper', to:'/collections/embossed' }] : []),
],
};
const rxEsc = s => s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
function repoint(body, list){
let n = 0;
for (const m of (list||[])) {
// <a href="FROM" ...> <img ... alt="LABEL"
const re = new RegExp(`(href=")${rxEsc(m.from)}("[^>]*>\\s*<img\\b[^>]*\\balt="${rxEsc(m.label)}")`, 'g');
body = body.replace(re, (_,a,b) => { n++; return a + m.to + b; });
}
return { body, n };
}
const here = new URL('.', import.meta.url).pathname;
const { swaps } = JSON.parse(fs.readFileSync(here + 'replacement-swaps.json','utf8'));
const GUARD = fs.readFileSync(here + 'error-guard.html','utf8');
if (!TOKEN) { console.error('❌ Set SHOPIFY_CONTENT_TOKEN (needs write_content scope).'); process.exit(1); }
const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type':'application/json' };
async function getPage(handle){
const r = await fetch(`${API}/pages.json?handle=${handle}`, { headers: H });
if (r.status === 403) throw new Error('403 — token lacks read_content scope');
if (!r.ok) throw new Error(`GET ${handle} -> HTTP ${r.status}`);
return (await r.json()).pages?.[0];
}
async function putBody(id, body){
const r = await fetch(`${API}/pages/${id}.json`, { method:'PUT', headers:H,
body: JSON.stringify({ page:{ id, body_html: body } }) });
if (!r.ok) throw new Error(`PUT ${id} -> HTTP ${r.status} ${await r.text()}`);
}
const enc = u => u.replace(/&/g,'&');
(async () => {
console.log(APPLY ? '⚙️ APPLY MODE — writing pages\n' : '🔎 DRY-RUN — no writes (add --apply)\n');
let grand = 0;
for (const handle of HANDLES) {
const page = await getPage(handle);
if (!page) { console.log(` ${handle}: NOT FOUND`); continue; }
let body = page.body_html || '';
const list = swaps[handle] || [];
let matched = 0, missed = 0;
for (const s of list) {
const variants = [s.old, enc(s.old)];
let hit = false;
for (const v of variants) {
if (v && body.includes(v)) { body = body.split(v).join(s.new); hit = true; }
}
hit ? matched++ : missed++;
}
let misN = 0;
if (DO_MIS) { const r = repoint(body, MISLABELS[handle]); body = r.body; misN = r.n; }
const guarded = /__dwImgGuard/.test(body);
if (!guarded) body += '\n' + GUARD;
grand += matched;
console.log(` ${handle}: swaps matched ${matched}/${list.length}${missed?` (missed ${missed})`:''}${DO_MIS?`, mislabels repointed ${misN}`:''}, guard ${guarded?'already present':'added'}, body ${(body.length/1024).toFixed(0)}KB`);
if (APPLY) { await putBody(page.id, body); console.log(` ✅ written`); }
}
console.log(`\n${APPLY?'Wrote':'Would write'} ${grand} image fixes across ${HANDLES.length} pages.`);
console.log('Note: this is the body_html BAND-AID (re-rots on next image churn; guard degrades gracefully).');
console.log('The permanent dynamic-template cure is the follow-up (DEPLOY-RUNBOOK.md).');
})().catch(e => { console.error('❌', e.message); process.exit(1); });