← back to Dw Domain Fleet
shared/catalog.js
79 lines
/**
* catalog.js — loads data/catalog.json once, exposes niche-filtered slices.
*/
const fs = require('fs');
const path = require('path');
const RAW = (() => {
try {
const d = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'catalog.json'), 'utf8'));
return Array.isArray(d) ? d : [];
} catch (e) {
console.error('[catalog] FATAL: could not load catalog.json —', e.message);
return [];
}
})();
// --- junk-product defense (dw-site-build standing rule) ---
const HOUSEHOLD = /lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i;
function isJunk(p) {
if (!p.image_url || !p.image_url.trim()) return true;
if (!p.handle) return true;
if (HOUSEHOLD.test(p.title || '')) return true;
if (/^\d{13,}$/.test(p.sku || '')) return true;
if (/display.?variant/i.test((p.tags || []).join(' '))) return true;
return false;
}
const CLEAN = RAW.filter(p => !isJunk(p));
// --- product-type normalization ---
function normType(t) {
if (!t) return 'Other';
const x = String(t).replace(/s$/i, '').trim();
if (/wallcovering/i.test(x)) return 'Wallcovering';
if (/wallpaper/i.test(x)) return 'Wallcovering';
if (/mural/i.test(x)) return 'Mural';
if (/fabric/i.test(x)) return 'Fabric';
if (/trim/i.test(x)) return 'Trim';
return x;
}
/**
* niche filter — positive keywords (any-match) minus negative keywords (any-match).
* Matches against title + tags + product_type. If pos is empty → all clean products.
*/
function nicheSlice({ pos = [], neg = [], types = [], limit = 4000 }) {
const P = pos.map(s => s.toLowerCase());
const N = neg.map(s => s.toLowerCase());
const T = types.map(s => s.toLowerCase());
const out = [];
for (const p of CLEAN) {
const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ') + ' ' + (p.product_type || '')).toLowerCase();
if (N.length && N.some(n => blob.includes(n))) continue;
if (T.length && !T.includes(normType(p.product_type).toLowerCase())) continue;
if (P.length && !P.some(k => blob.includes(k))) continue;
out.push(p);
if (out.length >= limit) break;
}
return out;
}
/**
* siteExtras — per-site catalog overlay. Returns the products in
* data/extras/<slug>.json (or [] if none). Used to add site-exclusive products
* (e.g. the AI-generated LA toile line on asseeninla) WITHOUT writing to the
* shared Shopify feed and WITHOUT being clobbered by pull-catalog. Each extra is
* a normal product object (title, handle, image_url, sku, product_type, tags,
* price, created_at) and may carry an optional buy_url for the sample CTA.
*/
function siteExtras(slug) {
if (!slug) return [];
try {
const d = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'data', 'extras', slug + '.json'), 'utf8'));
return Array.isArray(d) ? d.filter(p => p && p.image_url && p.handle) : [];
} catch { return []; }
}
module.exports = { RAW, CLEAN, isJunk, normType, nicheSlice, siteExtras, count: CLEAN.length };