← back to Yolo Agent
scripts/body-html-cleanup.js
149 lines
#!/usr/bin/env node
const path = require('path');
try { require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); } catch (_) {}
/**
* Body HTML Cleanup — Remove specs tables from product descriptions.
* Rewrites body_html to clean 3-sentence descriptions.
* Skips Phillip Jeffries (browse-hidden).
*/
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const API = `https://${SHOP}/admin/api/2024-10`;
const fs = require('fs');
if (!TOKEN) {
console.error('Missing SHOPIFY_ADMIN_TOKEN.');
process.exit(1);
}
const SPECS_PATTERNS = [
/<table[\s>]/i,
/<ul>\s*<li>\s*Width/i,
/Specifications/i,
/Material\s*:/i,
/Collection\s*:/i,
/<li>\s*Width/i,
/<li>\s*Content/i,
/\d+\s*in\.\s*wide\s*x\s*\d+/i,
/Colors may be/i,
/per single roll/i,
/printed on Demand/i,
];
const SKIP_VENDORS = ['Phillip Jeffries'];
async function shopifyGet(url) {
const resp = await fetch(url, {
headers: { 'X-Shopify-Access-Token': TOKEN }
});
if (resp.status === 429) {
const wait = parseFloat(resp.headers.get('Retry-After') || '4');
await new Promise(r => setTimeout(r, (wait + 1) * 1000));
return shopifyGet(url);
}
if (!resp.ok) throw new Error(`GET ${resp.status}: ${url}`);
return resp.json();
}
async function shopifyPut(url, data) {
await new Promise(r => setTimeout(r, 500));
const resp = await fetch(url, {
method: 'PUT',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (resp.status === 429) {
const wait = parseFloat(resp.headers.get('Retry-After') || '4');
await new Promise(r => setTimeout(r, (wait + 1) * 1000));
return shopifyPut(url, data);
}
if (!resp.ok) {
const body = await resp.text();
throw new Error(`PUT ${resp.status}: ${body.slice(0, 200)}`);
}
return resp.json();
}
function hasSpecs(bodyHtml) {
if (!bodyHtml) return false;
return SPECS_PATTERNS.some(p => p.test(bodyHtml));
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, ch => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[ch]));
}
function generateCleanBody(title, vendor) {
const parts = title.split('|').map(s => s.trim());
const nameColor = parts[0] || title;
const brand = parts[1] || vendor;
const nameParts = nameColor.split(' - ');
const pattern = nameParts[0] || nameColor;
const color = nameParts[1] || '';
const colorPhrase = color ? ` in ${escapeHtml(color)}` : '';
return `<p><strong>${escapeHtml(pattern)}</strong>${colorPhrase} by ${escapeHtml(brand)}. A premium wallcovering designed to bring refined elegance to any interior. Perfect for residential and commercial applications.</p>`;
}
async function run() {
const logFile = path.join(__dirname, '..', 'logs', 'body-html-cleanup.log');
let checked = 0, cleaned = 0, skipped = 0, failed = 0;
let sinceId = 0;
const LIMIT = 250;
const MAX_CLEAN = 500;
console.log(`Body HTML Cleanup — Starting at ${new Date().toISOString()}`);
fs.appendFileSync(logFile, `\n=== Run ${new Date().toISOString()} ===\n`);
while (cleaned < MAX_CLEAN) {
const url = `${API}/products.json?limit=${LIMIT}&fields=id,title,body_html,vendor,status&since_id=${sinceId}`;
let data;
try {
data = await shopifyGet(url);
} catch (e) {
console.error(`Fetch error: ${e.message}`);
break;
}
const products = data.products || [];
if (products.length === 0) break;
for (const p of products) {
checked++;
sinceId = p.id;
if (SKIP_VENDORS.includes(p.vendor)) { skipped++; continue; }
if (p.status === 'archived') { skipped++; continue; }
if (!hasSpecs(p.body_html)) continue;
const cleanBody = generateCleanBody(p.title, p.vendor);
try {
await shopifyPut(`${API}/products/${p.id}.json`, { product: { id: p.id, body_html: cleanBody } });
cleaned++;
const msg = `CLEANED: ${p.id} | ${p.title.slice(0, 50)} | ${p.vendor}`;
console.log(msg);
fs.appendFileSync(logFile, msg + '\n');
} catch (e) {
failed++;
console.error(`FAIL: ${p.id} | ${e.message.slice(0, 80)}`);
fs.appendFileSync(logFile, `FAIL: ${p.id} | ${e.message.slice(0, 80)}\n`);
}
await new Promise(r => setTimeout(r, 500));
}
if (checked % 1000 === 0) {
console.log(`Progress: checked=${checked} cleaned=${cleaned} skipped=${skipped} failed=${failed}`);
}
await new Promise(r => setTimeout(r, 300));
}
const summary = `DONE: checked=${checked} cleaned=${cleaned} skipped=${skipped} failed=${failed}`;
console.log(summary);
fs.appendFileSync(logFile, summary + '\n');
}
run().catch(e => { console.error('Fatal:', e); process.exit(1); });