← back to Designer Wallcoverings
auto-save: 2026-07-22T06:41:50 (1 files) — scripts/wallquest-refresh/harvest-wt-images.mjs
871d8d35fa97c43a672518641e7805d0289c2f2c · 2026-07-22 06:41:56 -0700 · Steve Abrams
Files touched
A scripts/wallquest-refresh/harvest-wt-images.mjs
Diff
commit 871d8d35fa97c43a672518641e7805d0289c2f2c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 06:41:56 2026 -0700
auto-save: 2026-07-22T06:41:50 (1 files) — scripts/wallquest-refresh/harvest-wt-images.mjs
---
scripts/wallquest-refresh/harvest-wt-images.mjs | 68 +++++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/scripts/wallquest-refresh/harvest-wt-images.mjs b/scripts/wallquest-refresh/harvest-wt-images.mjs
new file mode 100644
index 00000000..a3a466e6
--- /dev/null
+++ b/scripts/wallquest-refresh/harvest-wt-images.mjs
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+// Backfill harvester (NOT a vendor scraper / NOT a product creator): loads the
+// Winfield Thybony brand listing on kravet.com and collects {sku -> image} pairs,
+// then writes them into a scratch file. It NEVER creates Shopify products and
+// NEVER writes to dw_unified — the matched-attach step is separate + gated.
+// WT is a Kravet-family brand; the images we need for the 274 Fentucci-Naturals
+// (private-label-over-WT) products live on kravet.com's WT brand grid.
+// Usage: node harvest-wt-images.mjs (local Playwright, $0)
+import { chromium } from 'playwright';
+import { writeFileSync } from 'node:fs';
+
+const BASE = 'https://www.kravet.com/wallcovering?brand=Winfield%20Thybony';
+const OUT = '/tmp/wt-image-harvest.json';
+const wanted = new Set(process.env.SKUS ? process.env.SKUS.split(',') : []);
+
+const norm = s => String(s || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
+const collected = new Map(); // normSku -> {sku, image, url}
+
+const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
+try {
+ const page = await browser.newPage({ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36' });
+ let pageNum = 1, emptyStreak = 0;
+ while (pageNum <= 60) {
+ const url = `${BASE}&p=${pageNum}`;
+ try {
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ } catch (e) { console.error(`page ${pageNum} nav fail: ${e.message}`); break; }
+ // Kravet is Algolia InstantSearch — wait for hits to hydrate
+ await page.waitForTimeout(6000);
+ const hits = await page.evaluate(() => {
+ const out = [];
+ const cards = document.querySelectorAll('.ais-Hits-item, .ais-InfiniteHits-item, li[class*="Hits-item"], [class*="product-item"]');
+ for (const c of cards) {
+ const a = c.querySelector('a[href]');
+ const img = c.querySelector('img');
+ const href = a ? a.href : '';
+ // pull a candidate SKU from any sku-ish text or the href tail
+ let sku = '';
+ const skuEl = c.querySelector('[class*="sku"], [class*="Sku"], [class*="product-code"]');
+ if (skuEl) sku = skuEl.textContent.trim();
+ if (!sku && href) { const m = href.match(/([A-Z0-9.\-]{4,})\/?$/i); if (m) sku = m[1]; }
+ let src = '';
+ if (img) src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-lazy') || '';
+ out.push({ sku, image: src, url: href, text: (c.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120) });
+ }
+ return out;
+ });
+ let added = 0;
+ for (const h of hits) {
+ // try to recover a real WT sku from the card text (C####T / GT####T / EG### / FAB### / MG#### / WHF####)
+ const m = (h.sku + ' ' + h.text).match(/\b(C\d{3,4}[A-Z]?T?|GT\d{3,4}[A-Z]?|EG\d{3,4}[A-Z]*|FAB\d{3,4}|MG\d{3,4}|WHF\d{3,4}|[A-Z]{2,4}\d{3,5}[A-Z]?)\b/);
+ const sku = m ? m[1] : h.sku;
+ if (!sku || !h.image || !/^https?:/.test(h.image)) continue;
+ const k = norm(sku);
+ if (!collected.has(k)) { collected.set(k, { sku, image: h.image, url: h.url }); added++; }
+ }
+ console.log(`page ${pageNum}: ${hits.length} cards, +${added} new (total ${collected.size})`);
+ if (added === 0) { emptyStreak++; if (emptyStreak >= 2) break; } else emptyStreak = 0;
+ pageNum++;
+ }
+ const arr = [...collected.values()];
+ writeFileSync(OUT, JSON.stringify(arr, null, 1));
+ const matched = wanted.size ? arr.filter(a => wanted.has(norm(a.sku))).length : arr.length;
+ console.log(`\nHARVEST DONE: ${arr.length} unique WT {sku,image} pairs -> ${OUT}`);
+ if (wanted.size) console.log(`of the ${wanted.size} wanted SKUs, matched ${matched}`);
+} finally {
+ await browser.close();
+}
← 46021776 auto-save: 2026-07-22T05:11:29 (1 files) — shopify/scripts/c
·
back to Designer Wallcoverings
·
no-image backfill: attach 18 verified vendor images (src + b 770e4cf5 →