← back to Reidwitlin Landing
auto-save: 2026-07-07T12:30:21 (4 files) — lib/backfill-source-images.js lib/relabel-colorways.js scripts/backfill-source-images.js scripts/build-rwltd-data.js
b8b384a7cb37a63759684a41726f200a64f23abb · 2026-07-07 12:30:22 -0700 · Steve Abrams
Files touched
M lib/backfill-source-images.jsM lib/relabel-colorways.jsM scripts/backfill-source-images.jsM scripts/build-rwltd-data.js
Diff
commit b8b384a7cb37a63759684a41726f200a64f23abb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 7 12:30:22 2026 -0700
auto-save: 2026-07-07T12:30:21 (4 files) — lib/backfill-source-images.js lib/relabel-colorways.js scripts/backfill-source-images.js scripts/build-rwltd-data.js
---
lib/backfill-source-images.js | 27 +++++++++++++++++++--------
lib/relabel-colorways.js | 2 +-
scripts/backfill-source-images.js | 14 +++++++-------
scripts/build-rwltd-data.js | 4 ++--
4 files changed, 29 insertions(+), 18 deletions(-)
diff --git a/lib/backfill-source-images.js b/lib/backfill-source-images.js
index 1756b57..077086f 100644
--- a/lib/backfill-source-images.js
+++ b/lib/backfill-source-images.js
@@ -15,6 +15,8 @@
*/
const https = require('https');
+const MAX_PAGES = 6; // rwltd.com typically 3–5 pages; 6 is a safe ceiling
+
// MUST match build-rwltd-data.js's slug: drop apostrophes BEFORE the char-class
// collapse so "I'm Crushed" -> "im-crushed" (matching the vendor mfr_sku), not
// "i-m-crushed". A divergence here silently breaks the feed-key<->sku lookup for
@@ -27,7 +29,9 @@ function get(url) {
return new Promise((res, rej) => {
https.get(url, opts, r => {
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location) return res(get(r.headers.location));
- let d = ''; r.on('data', c => d += c); r.on('end', () => res(d));
+ let d = '';
+ r.on('data', c => { d += c; });
+ r.on('end', () => res(d));
}).on('error', rej);
});
}
@@ -35,16 +39,18 @@ function get(url) {
// Pull the full source feed and build slug("pattern-option") -> featured_image src.
async function fetchSourceImageIndex(base = 'https://rwltd.com/products.json?limit=250') {
const feed = [];
- for (let page = 1; page <= 6; page++) {
+ for (let page = 1; page <= MAX_PAGES; page++) {
let ps = [];
try { ps = JSON.parse(await get(`${base}&page=${page}`)).products || []; } catch { break; }
if (!ps.length) break;
feed.push(...ps);
}
const idx = {};
- for (const p of feed) for (const v of (p.variants || [])) {
- const fi = v.featured_image && v.featured_image.src;
- if (fi) idx[slug(`${p.title}-${v.option1}`)] = fi;
+ for (const p of feed) {
+ for (const v of (p.variants || [])) {
+ const fi = v.featured_image?.src;
+ if (fi) idx[slug(`${p.title}-${v.option1}`)] = fi;
+ }
}
return { idx, patterns: feed.length };
}
@@ -58,16 +64,21 @@ async function fetchSourceImageIndex(base = 'https://rwltd.com/products.json?lim
*/
async function backfillFromSourceFeed(products, opts = {}) {
const { idx, patterns } = await fetchSourceImageIndex(opts.base);
- let backfilled = 0; const conflicts = [];
+ let backfilled = 0;
+ const conflicts = [];
for (const p of products) {
const src = idx[slug(p.sku)];
if (!src) continue;
const noPhoto = p.image_note || !(p.images && p.images.length);
if (noPhoto) {
- p.images = [src]; p.swatch = src; p.room = src; delete p.image_note;
+ p.images = [src];
+ p.swatch = src;
+ p.room = src;
+ delete p.image_note;
p.image_source = 'rwltd-variant'; backfilled++;
} else {
- const cur = (p.swatch || '').split('?')[0], s0 = src.split('?')[0];
+ const cur = (p.swatch || '').split('?')[0];
+ const s0 = src.split('?')[0];
if (cur !== s0) conflicts.push({ handle: p.handle, color: p.color, ours: cur.split('/').pop(), source: s0.split('/').pop() });
}
}
diff --git a/lib/relabel-colorways.js b/lib/relabel-colorways.js
index 94e736b..1bd7c9f 100644
--- a/lib/relabel-colorways.js
+++ b/lib/relabel-colorways.js
@@ -48,7 +48,7 @@ function relabelUnreliableSeries(products, { MIN_N = 4, MIN_HUES = 4 } = {}) {
for (const [series, ps] of bySeries) {
const labels = new Set(ps.map(p => norm(p.color)).filter(Boolean));
- const hexes = new Set(ps.map(p => (p.hex || '').toUpperCase()).filter(Boolean));
+ const hexes = new Set(ps.map(p => p.hex?.toUpperCase()).filter(Boolean));
const unreliable = ps.length >= MIN_N && labels.size === 1 && hexes.size >= MIN_HUES;
if (!unreliable) continue;
diff --git a/scripts/backfill-source-images.js b/scripts/backfill-source-images.js
index 8844ce7..5f158de 100644
--- a/scripts/backfill-source-images.js
+++ b/scripts/backfill-source-images.js
@@ -36,17 +36,17 @@ const BUCKET_ORDER = ['white','grey','black','pink','red','orange','brown','gold
snap.drafted_no_own_photo = snap.drafts.length;
// Recompute facets from the NEW published set (the partition changed which are live).
- const fb = {}, fsr = {}, fc = {};
+ const bookCounts = {}, seriesCounts = {}, colorCounts = {};
for (const p of snap.products) {
- if (p.book) fb[p.book] = (fb[p.book] || 0) + 1;
- if (p.series) fsr[p.series] = (fsr[p.series] || 0) + 1;
- if (p.color_bucket) fc[p.color_bucket] = (fc[p.color_bucket] || 0) + 1;
+ if (p.book) bookCounts[p.book] = (bookCounts[p.book] || 0) + 1;
+ if (p.series) seriesCounts[p.series] = (seriesCounts[p.series] || 0) + 1;
+ if (p.color_bucket) colorCounts[p.color_bucket] = (colorCounts[p.color_bucket] || 0) + 1;
}
snap.facets = {
total: snap.products.length,
- books: Object.entries(fb).sort((a, b) => b[1] - a[1]),
- series: Object.entries(fsr).sort((a, b) => b[1] - a[1]).slice(0, 300),
- colors: BUCKET_ORDER.filter(b => fc[b]).map(b => [b, fc[b]]),
+ books: Object.entries(bookCounts).sort((a, b) => b[1] - a[1]),
+ series: Object.entries(seriesCounts).sort((a, b) => b[1] - a[1]).slice(0, 300),
+ colors: BUCKET_ORDER.filter(b => colorCounts[b]).map(b => [b, colorCounts[b]]),
};
console.log(`backfilled ${backfilled} colorways | published ${snap.products.length} | drafted ${snap.drafts.length} | facets.total ${snap.facets.total}`);
diff --git a/scripts/build-rwltd-data.js b/scripts/build-rwltd-data.js
index 40bf589..9bb46ce 100644
--- a/scripts/build-rwltd-data.js
+++ b/scripts/build-rwltd-data.js
@@ -327,8 +327,8 @@ async function main() {
// label lies. Relabel from the pixel-derived colour family so each card's text
// matches its image. Handles/SKUs untouched — links still hit the same product.
const { relabelUnreliableSeries } = require('../lib/relabel-colorways');
- const rl = relabelUnreliableSeries(products);
- if (rl.productsFixed) console.log(` colorway labels: relabelled ${rl.productsFixed} in ${rl.seriesFixed} mis-stamped series (${rl.series.join(', ')})`);
+ const relabel = relabelUnreliableSeries(products);
+ if (relabel.productsFixed) console.log(` colorway labels: relabelled ${relabel.productsFixed} in ${relabel.seriesFixed} mis-stamped series (${relabel.series.join(', ')})`);
// Source-image backfill — a FOURTH pass, BEFORE the own-photo partition. The
// scrape lost the per-colorway image mapping; the free rwltd.com Shopify feed
← d67eec9 reidwitlin: contrarian FIX-THEN-SHIP — guard silent-zero-res
·
back to Reidwitlin Landing
·
chore: lint, refactor, v0.2.3 (session close) d7714e5 →