← back to All Dw Landing
scripts/build-sites-json.js
123 lines
#!/usr/bin/env node
/**
* all.designerwallcoverings.com — build data/sites.json
*
* READ-ONLY query against the LOCAL dw_unified mirror: every distinct vendor
* with active products (count + one sample product image), slugified to its
* candidate <slug>.designerwallcoverings.com subdomain, merged with the
* hand-maintained config/known-subdomains.json (existing microsites +
* non-brand internal subdomains, marked type:"internal").
*
* Output: data/sites.json [{slug, vendor, activeProducts, sampleImage, type, knownProject?, ...}]
* $0 (local PG read).
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Pool } = require('pg');
const OUT = path.join(__dirname, '..', 'data', 'sites.json');
const KNOWN = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config', 'known-subdomains.json'), 'utf8'));
// Same connection pattern as astek-landing/scripts/build-products-json.js
const PW = (() => {
try {
const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
const m = env.match(/^DW_ADMIN_DB_PASSWORD=(.*)$/m);
if (m) return m[1].replace(/^["']|["']$/g, '').trim();
} catch {}
return process.env.PGPASSWORD || '';
})();
const pool = process.env.DATABASE_URL
? new Pool({ connectionString: process.env.DATABASE_URL })
: new Pool({ host: '127.0.0.1', port: 5432, user: 'dw_admin', database: 'dw_unified', password: PW });
// Banned-word scrub on every DISPLAY string (standing rule). Raw vendor stays
// internal for slug derivation only.
const clean = s => (s == null ? s : String(s)
.replace(/\bWallpapers\b/gi, 'Wallcoverings')
.replace(/\bWallpaper\b/gi, 'Wallcovering'));
// vendor -> candidate subdomain slug: lowercase, alphanumeric only.
const slugify = v => String(v || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/&/g, 'and').replace(/[^a-z0-9]/g, '');
async function main() {
// One row per vendor with ≥1 ACTIVE product; sample image = most recent non-null image.
const { rows } = await pool.query(`
SELECT vendor,
COUNT(*)::int AS active_products,
(ARRAY_REMOVE(ARRAY_AGG(image_url ORDER BY created_at_shopify DESC NULLS LAST), NULL))[1] AS sample_image
FROM shopify_products
WHERE UPPER(status) = 'ACTIVE' AND vendor IS NOT NULL AND vendor <> ''
GROUP BY vendor
ORDER BY COUNT(*) DESC
`);
const knownBrands = new Map((KNOWN.brands || []).map(b => [b.slug, b]));
const sites = [];
const seenSlugs = new Set();
for (const r of rows) {
const slug = slugify(r.vendor);
if (!slug || seenSlugs.has(slug)) { if (seenSlugs.has(slug)) console.warn(` ! slug collision skipped: ${r.vendor} -> ${slug}`); continue; }
seenSlugs.add(slug);
const k = knownBrands.get(slug) || {};
sites.push({
slug,
vendor: clean(r.vendor),
activeProducts: r.active_products,
sampleImage: r.sample_image || null,
type: 'brand',
hasMicrosite: !!k.hasMicrosite,
knownProject: k.knownProject || null,
localUrl: k.localUrl || null,
note: k.note || null,
});
}
// Known brand sites whose vendor slug didn't come out of the active query still get a card.
for (const [slug, k] of knownBrands) {
if (seenSlugs.has(slug)) continue;
seenSlugs.add(slug);
sites.push({
slug, vendor: clean(k.vendor || slug), activeProducts: 0, sampleImage: null,
type: 'brand', hasMicrosite: !!k.hasMicrosite,
knownProject: k.knownProject || null, localUrl: k.localUrl || null, note: k.note || null,
});
}
// Non-brand internal subdomains.
for (const i of (KNOWN.internal || [])) {
if (seenSlugs.has(i.slug)) continue;
seenSlugs.add(i.slug);
sites.push({
slug: i.slug, vendor: clean(i.label || i.slug), activeProducts: null, sampleImage: null,
type: 'internal', hasMicrosite: null, knownProject: i.knownProject || null,
localUrl: i.localUrl || null, note: i.note || null,
});
}
const brands = sites.filter(s => s.type === 'brand');
const activeBrands = brands.filter(s => s.activeProducts > 0);
const withMicrosite = activeBrands.filter(s => s.hasMicrosite);
const snapshot = {
built_at: new Date().toISOString(),
source: 'dw_unified.shopify_products (local mirror, read-only) + config/known-subdomains.json',
counts: {
sites: sites.length,
brands: brands.length,
activeBrands: activeBrands.length,
internal: sites.length - brands.length,
brandsWithMicrosite: withMicrosite.length,
},
sites,
};
fs.writeFileSync(OUT, JSON.stringify(snapshot, null, 1));
console.log(`sites.json -> ${OUT}`);
console.log(` sites: ${sites.length} (brands: ${brands.length}, active brands: ${activeBrands.length}, internal: ${sites.length - brands.length})`);
console.log(` rollout: ${withMicrosite.length} of ${activeBrands.length} active brands have a microsite (${(100 * withMicrosite.length / Math.max(1, activeBrands.length)).toFixed(1)}%)`);
await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });