[object Object]

← 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

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 →