[object Object]

← 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

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 →