← back to Dw Yolo Loop
Vendor-landing factory: build-line.js generates a branded landing for ANY live DW vendor from Shopify
5af2df54975183ec102bf4859520239e801c9693 · 2026-06-12 10:54:40 -0700 · Steve Abrams
- build-line.js <Vendor> → snapshot (CDN image urls) + handles map + site.config block; no image download needed
- server localImages passthrough for non-local lines (CFG.localImages flag)
- variant picker uses highest non-sample price (never reads the $4.25 sample as the product price — $4.25 policy)
- validated on Sarah Bartholomew (sample-only edge handled correctly)
Files touched
A artmura-site/build-line.jsM artmura-site/server.jsM artmura-site/site.config.js
Diff
commit 5af2df54975183ec102bf4859520239e801c9693
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jun 12 10:54:40 2026 -0700
Vendor-landing factory: build-line.js generates a branded landing for ANY live DW vendor from Shopify
- build-line.js <Vendor> → snapshot (CDN image urls) + handles map + site.config block; no image download needed
- server localImages passthrough for non-local lines (CFG.localImages flag)
- variant picker uses highest non-sample price (never reads the $4.25 sample as the product price — $4.25 policy)
- validated on Sarah Bartholomew (sample-only edge handled correctly)
---
artmura-site/build-line.js | 105 ++++++++++++++++++++++++++++++++++++++++++++
artmura-site/server.js | 2 +
artmura-site/site.config.js | 1 +
3 files changed, 108 insertions(+)
diff --git a/artmura-site/build-line.js b/artmura-site/build-line.js
new file mode 100644
index 0000000..89b296e
--- /dev/null
+++ b/artmura-site/build-line.js
@@ -0,0 +1,105 @@
+#!/usr/bin/env node
+/**
+ * DW vendor-landing generator — build a branded editorial landing for ANY live DW vendor
+ * straight from the Shopify store. The "fascinating for other lines" factory.
+ *
+ * SHOPIFY_ADMIN_TOKEN=… node build-line.js "Schumacher"
+ *
+ * Produces:
+ * lines/<slug>.json catalog snapshot (artmura.json shape, CDN image urls)
+ * lines/<slug>-handles.json {sku: live shopify handle}
+ * prints a site.config.js block to paste in.
+ * Then: VENDOR=<slug> PORT=99xx node server.js (set dataFile/colorsFile/imagePrefix per the printed block)
+ */
+const fs = require('fs');
+const path = require('path');
+
+const STORE = process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const API = '2024-10';
+const VENDOR = process.argv[2];
+if (!TOKEN || !VENDOR) { console.error('usage: SHOPIFY_ADMIN_TOKEN=… node build-line.js "<Vendor>"'); process.exit(1); }
+const slug = VENDOR.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
+const OUT = path.join(__dirname, 'lines');
+fs.mkdirSync(OUT, { recursive: true });
+
+function api(p) {
+ return fetch(`https://${STORE}/admin/api/${API}${p}`, { headers: { 'X-Shopify-Access-Token': TOKEN } });
+}
+async function getAll() {
+ let url = `/products.json?vendor=${encodeURIComponent(VENDOR)}&status=active&limit=250`;
+ const out = [];
+ while (url) {
+ const res = await api(url);
+ const link = res.headers.get('Link') || '';
+ const d = await res.json();
+ out.push(...(d.products || []));
+ const m = link.split(',').find(s => s.includes('rel="next"'));
+ url = m ? m.slice(m.indexOf('<') + 1, m.indexOf('>')).replace(/^https:\/\/[^/]+\/admin\/api\/[^/]+/, '') : null;
+ }
+ return out;
+}
+async function metafields(pid) {
+ try { const d = await (await api(`/products/${pid}/metafields.json`)).json();
+ return Object.fromEntries((d.metafields || []).filter(m => m.namespace === 'custom').map(m => [m.key, m.value])); }
+ catch { return {}; }
+}
+
+(async () => {
+ console.log(`building line "${VENDOR}" from ${STORE} ...`);
+ const prods = await getAll();
+ console.log(` ${prods.length} active products`);
+ const products = [], handles = {};
+ for (const p of prods) {
+ const mf = await metafields(p.id);
+ const isSample = v => (v.option1 || '').toLowerCase() === 'sample' || /-sample$/i.test(v.sku || '');
+ const sample = p.variants.find(isSample);
+ // main = highest-priced non-sample variant (never read the $4.25 sample as the price — see $4.25 policy)
+ const nonSample = p.variants.filter(v => !isSample(v));
+ const yard = nonSample.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))[0] || p.variants[0];
+ const sku = (yard.sku || '').trim();
+ const tags = (p.tags || '').split(',').map(s => s.trim()).filter(Boolean);
+ const baseTitle = p.title.replace(/\s*\|\s*[^|]+$/, '').replace(/\s+Wallcovering$/i, '');
+ if (sku) handles[sku.toUpperCase()] = p.handle;
+ products.push({
+ mfr_sku: mf.mfr_sku || sku,
+ pattern_series: mf.design_name || tags[0] || baseTitle.split(' ')[0],
+ color: mf.colorway_name || baseTitle.split(' ').slice(1).join(' ') || null,
+ title: baseTitle,
+ collection_book: mf.collection || null,
+ tags,
+ product_type: p.product_type || 'Wallcoverings',
+ price_newwall_retail: parseFloat(yard.price) || 0,
+ sample_price: sample ? parseFloat(sample.price) : null,
+ sold_by: yard.option1 || 'Yard',
+ substrate: mf.material || null,
+ grade: mf.grade || null,
+ dimensions: mf.width || null,
+ wall_coverage: mf.wall_coverage || null,
+ lead_time: mf.lead_time || null,
+ origin: mf.origin || null,
+ body_html: p.body_html,
+ product_url: `https://www.designerwallcoverings.com/products/${p.handle}`,
+ handle: p.handle,
+ shopify_source_id: p.id,
+ images: (p.images || []).map(im => im.src.split('?')[0]),
+ image_count: (p.images || []).length,
+ published_at: p.published_at,
+ });
+ }
+ fs.writeFileSync(path.join(OUT, `${slug}.json`), JSON.stringify({ vendor: VENDOR, captured_count: products.length, products }, null, 2));
+ fs.writeFileSync(path.join(OUT, `${slug}-handles.json`), JSON.stringify(handles, null, 1));
+ console.log(` → lines/${slug}.json (${products.length}) lines/${slug}-handles.json`);
+ console.log(`\nPaste into site.config.js:\n`);
+ console.log(` ${slug}: {
+ vendor: '${VENDOR}', title: '${VENDOR} — Designer Wallcoverings',
+ wordmark: '${VENDOR}', kicker: 'Designer Wallcoverings', tagline: '${VENDOR} collection.',
+ booksHeading: '${VENDOR}', metaDescription: '${VENDOR} wallcoverings at Designer Wallcoverings.',
+ dataFile: 'artmura-site/lines/${slug}.json',
+ colorsFile: 'artmura-site/lines/${slug}-colors.json', // optional: run extract_colors variant
+ imagePrefix: '', localImages: false, // uses Shopify CDN urls directly
+ storeBase: 'https://www.designerwallcoverings.com',
+ palette: { bg: '#f6f2ec', ink: '#211d18', accent: '#8c7a5f', gold: '#a98c54' },
+ },`);
+ console.log(`\nThen: VENDOR=${slug} PORT=99xx node server.js`);
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/artmura-site/server.js b/artmura-site/server.js
index 02aa1f5..9ce2747 100644
--- a/artmura-site/server.js
+++ b/artmura-site/server.js
@@ -19,8 +19,10 @@ const PORT = process.env.PORT || 9921;
const app = express();
// --- load + normalize catalog ---
+// Artmura has downloaded local images (CFG.localImages); generic lines use CDN URLs directly.
function localImages(r) {
return (r.images || []).map((src, i) => {
+ if (!CFG.localImages) return src; // pass CDN url straight through
const base = src.split('/').pop().split('?')[0];
const ext = path.extname(base) || '.jpg';
return `/images/${r.mfr_sku}-${i + 1}${ext}`;
diff --git a/artmura-site/site.config.js b/artmura-site/site.config.js
index f727f0b..cf3b1b4 100644
--- a/artmura-site/site.config.js
+++ b/artmura-site/site.config.js
@@ -15,6 +15,7 @@ module.exports = {
dataFile: 'scripts/artmura-onboard/data/artmura.json',
colorsFile: 'scripts/artmura-onboard/data/artmura-colors.json',
imagePrefix: '.newwall-ref/artmura/images',
+ localImages: true, // Artmura images were downloaded locally; generic lines use CDN urls
// live store: cards/PDP CTAs link OUT to the real Shopify product for purchase
storeBase: 'https://www.designerwallcoverings.com',
palette: { bg: '#f6f2ec', ink: '#211d18', accent: '#8c7a5f', gold: '#a98c54' },
← 8489a63 Vendor-landing template: config-driven + links to live Shopi
·
back to Dw Yolo Loop
·
Artmura site README: document the clone-a-line template flow 8a03eb4 →