← back to Carmelwallpapers

scripts/pull-from-shopify.js

106 lines

/**
 * pull-from-shopify.js
 * Paginate DW /products.json, filter to coastal/natural/botanical wallcoverings,
 * and write to data/products.json.
 * Run: node scripts/pull-from-shopify.js
 */

const https = require('https');
const fs = require('fs');
const path = require('path');
const { classifyAesthetic } = require('../../_shared/microsite-aesthetic');
const SITE_CFG_RAILS = (() => { try { return require('../site.config.json').rails || []; } catch { return []; } })();

// Niche tags — match any of these
const COASTAL_TAGS = /^(grasscloth|sisal|seagrass|jute|linen|raffia|rattan|bamboo|woven|coastal|botanical|natural|organic|hemp|cane|abaca|rush|wicker|palm|sea grass|boucle|bouclé|texture|handwoven|hand woven|natural fiber|fiber|earthy|neutral|sand|stone|stone wash|pebble|driftwood|shell|coral)$/i;

// Title keyword match
const COASTAL_TITLE = /\b(grasscloth|sisal|seagrass|jute|linen|raffia|rattan|bamboo|coastal|botanical|shell|coral|palm|wave|sand|sea|driftwood|pebble|woven|natural|organic|hemp|cane|abaca|rush|wicker|texture|reed|twig|leaf|fern|kelp|tide|shore|lagoon|bay|dune)\b/i;

// Reject non-wall products
const REJECT_TITLE = /(lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine|stool|table|chair|sofa|curtain|bedding|towel|plate|bowl|mug)/i;

function fetchPage(page) {
  return new Promise((resolve, reject) => {
    const url = `https://designerwallcoverings.com/products.json?limit=250&page=${page}`;
    const doGet = (u) => {
      https.get(u, { headers: { 'User-Agent': 'carmelwallpapers-builder/1.0' } }, (res) => {
        if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
          return doGet(res.headers.location);
        }
        let data = '';
        res.on('data', c => data += c);
        res.on('end', () => {
          try { resolve(JSON.parse(data).products || []); }
          catch(e) { reject(new Error(`JSON parse fail on page ${page} (status ${res.statusCode}): ${e.message}\nBody: ${data.slice(0,200)}`)); }
        });
      }).on('error', reject);
    };
    doGet(url);
  });
}

function aestheticOf(tags, title) {
  const t = (tags || []).map(x => x.toLowerCase()).join(' ');
  const tt = (title || '').toLowerCase();
  const all = t + ' ' + tt;
  if (/grasscloth|sisal|seagrass|jute|abaca|rush|cane|rattan|wicker/.test(all)) return 'grasscloth';
  if (/linen|bouclé|boucle|woven|handwoven/.test(all)) return 'linen';
  if (/botanical|palm|fern|leaf|floral|garden|tropical|frond|vine/.test(all)) return 'botanical';
  if (/coastal|shell|coral|wave|shore|tide|sea |beach|dune|nautical|lagoon|bay/.test(all)) return 'coastal';
  if (/raffia|bamboo|hemp|organic|natural fiber|fiber|reed|twig/.test(all)) return 'raffia';
  return 'natural';
}

function classifyForSite(tags, title) {
  const railsMatch = classifyAesthetic(tags, SITE_CFG_RAILS);
  if (railsMatch && railsMatch !== 'all') return railsMatch;
  return aestheticOf(tags, title);
}

(async () => {
  console.log('Pulling DW /products.json (up to 30 pages)…');
  const all = [];
  for (let page = 1; page <= 30; page++) {
    const products = await fetchPage(page);
    if (!products.length) break;
    all.push(...products);
    process.stdout.write(`  page ${page}: ${products.length} products (total ${all.length})\n`);
    if (products.length < 250) break;
    await new Promise(r => setTimeout(r, 300)); // gentle rate limit
  }
  console.log(`\nTotal fetched: ${all.length}`);

  const coastal = all
    .filter(p => p.product_type && /wallcovering/i.test(p.product_type))
    .filter(p => !REJECT_TITLE.test(p.title || ''))
    .filter(p => p.images && p.images.length > 0 && p.images[0].src)
    .filter(p => {
      const hasTag = (p.tags || []).some(t => COASTAL_TAGS.test(t));
      const hasTitle = COASTAL_TITLE.test(p.title || '');
      return hasTag || hasTitle;
    })
    .map(p => ({
      handle: p.handle,
      title: p.title,
      vendor: (p.vendor || '').trim(),
      product_type: p.product_type,
      image_url: p.images[0].src,
      images: p.images.slice(0, 4).map(i => i.src),
      tags: p.tags || [],
      aesthetic: classifyForSite(p.tags, p.title),
      product_url: `https://designerwallcoverings.com/products/${p.handle}`,
      sample_url: `https://designerwallcoverings.com/products/${p.handle}#sample`,
    }));

  console.log(`Coastal filtered: ${coastal.length}`);

  const byA = {};
  for (const p of coastal) byA[p.aesthetic] = (byA[p.aesthetic] || 0) + 1;
  console.log('Aesthetics breakdown:', byA);

  const outPath = path.join(__dirname, '..', 'data', 'products.json');
  fs.writeFileSync(outPath, JSON.stringify(coastal, null, 2));
  console.log(`Wrote ${coastal.length} products to ${outPath}`);
})().catch(e => { console.error(e); process.exit(1); });