← back to 1960swallpaper
stage2: populate 1960swallpaper catalog + fix niche filter
8ec2cc413400e281db64ab5c804b08eda483db41 · 2026-05-19 11:10:01 -0700 · SteveStudio2
- Inserted 150 genuine 1960s-era wallcoverings into dw_unified.microsite_products
(pop art, op art, psychedelic, space-age, mid-century modern) pulled from the
DW master catalog; site previously had only 3 off-niche Circus Damask rows.
- Fixed site.config.json rails: hyphenated 'pop-art'/'op-art' and bare 'mod'
never whole-word-matched the actual tags ('Pop Art', 'Mid-Century Modern'),
so every product fell into the 'all' aesthetic bucket. Rails now
modern/pop/psychedelic/floral/geometric/retro — buckets populate correctly.
- Broadened server.js NICHE_POS to include mid-century/space-age/retro.
- Added scripts/populate-catalog.js (idempotent, deterministic).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Files touched
A scripts/populate-catalog.jsM server.jsM site.config.json
Diff
commit 8ec2cc413400e281db64ab5c804b08eda483db41
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date: Tue May 19 11:10:01 2026 -0700
stage2: populate 1960swallpaper catalog + fix niche filter
- Inserted 150 genuine 1960s-era wallcoverings into dw_unified.microsite_products
(pop art, op art, psychedelic, space-age, mid-century modern) pulled from the
DW master catalog; site previously had only 3 off-niche Circus Damask rows.
- Fixed site.config.json rails: hyphenated 'pop-art'/'op-art' and bare 'mod'
never whole-word-matched the actual tags ('Pop Art', 'Mid-Century Modern'),
so every product fell into the 'all' aesthetic bucket. Rails now
modern/pop/psychedelic/floral/geometric/retro — buckets populate correctly.
- Broadened server.js NICHE_POS to include mid-century/space-age/retro.
- Added scripts/populate-catalog.js (idempotent, deterministic).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---
scripts/populate-catalog.js | 80 +++++++++++++++++++++++++++++++++++++++++++++
server.js | 6 ++--
site.config.json | 8 ++---
3 files changed, 88 insertions(+), 6 deletions(-)
diff --git a/scripts/populate-catalog.js b/scripts/populate-catalog.js
new file mode 100644
index 0000000..3053ccf
--- /dev/null
+++ b/scripts/populate-catalog.js
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+/**
+ * Stage-2 one-shot: populate dw_unified.microsite_products for 1960swallpaper.
+ * Pulls genuine 1960s-era wallcoverings from the DW master catalog
+ * (shopify_products) and inserts them so the site's niche filter has stock.
+ */
+const fs = require('fs');
+const path = require('path');
+const { Pool } = require(path.join(__dirname, '..', '..', '_shared', 'node_modules', 'pg'));
+const { classifyAesthetic } = require(path.join(__dirname, '..', '..', '_shared', 'microsite-aesthetic.js'));
+
+const siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'site.config.json'), 'utf8'));
+const SITE_SLUG = siteCfg.slug;
+const RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
+const CAP = 150;
+
+const pool = new Pool({ host: process.env.PGHOST || '/tmp', database: 'dw_unified' });
+
+(async () => {
+ // Strongest 1960s signal first (rank 0), mid-century modern last (rank 1),
+ // so a 150-cap leans toward the most on-theme stock.
+ const { rows } = await pool.query(`
+ SELECT DISTINCT ON (coalesce(sku, handle))
+ sku, handle, title, vendor, product_type, image_url, tags,
+ retail_price, price,
+ CASE WHEN title ~* '1960|\\mpop art\\M|\\mop art\\M|psychedelic|space.?age|\\matomic\\M'
+ OR tags ~* '1960|pop art|op art|psychedelic|space.?age|atomic'
+ THEN 0 ELSE 1 END AS rank
+ FROM shopify_products
+ WHERE status = 'ACTIVE' AND image_url IS NOT NULL AND image_url <> ''
+ AND product_type ILIKE '%Wallcovering%'
+ AND title !~* 'lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine'
+ AND (title ~* '1960|\\mpop art\\M|\\mop art\\M|psychedelic|space.?age|\\matomic\\M'
+ OR tags ~* '1960|pop art|op art|psychedelic|space.?age|atomic|mid.century modern')
+ AND lower(title || ' ' || coalesce(tags,'')) ~ 'mod|op art|op-art|pop|geometric|psychedelic|1960'
+ AND lower(title || ' ' || coalesce(tags,'')) !~ 'solid|blank|plain'
+ ORDER BY coalesce(sku, handle)
+ `);
+
+ // Re-sort by rank (strongest signal first), stable tiebreak on sku/handle,
+ // then trim to cap — deterministic so re-runs pick the same 150.
+ rows.sort((a, b) => a.rank - b.rank
+ || String(a.sku || a.handle).localeCompare(String(b.sku || b.handle)));
+ const picked = rows.slice(0, CAP);
+
+ // Idempotent: drop any prior catalog-sourced rows for this site so a re-run
+ // with a changed query/cap doesn't leave stale extras behind.
+ await pool.query(
+ "DELETE FROM microsite_products WHERE site_slug=$1 AND source='catalog'",
+ [SITE_SLUG]);
+
+ let inserted = 0;
+ for (let i = 0; i < picked.length; i++) {
+ const p = picked[i];
+ const tags = typeof p.tags === 'string'
+ ? p.tags.split(',').map(t => t.trim()).filter(Boolean)
+ : (Array.isArray(p.tags) ? p.tags : []);
+ const aesthetic = classifyAesthetic(tags, RAILS);
+ const skuKey = p.sku || p.handle;
+ await pool.query(`
+ INSERT INTO microsite_products
+ (site_slug, sku, source, title, vendor, product_type, handle,
+ image_url, tags, aesthetic, max_price, product_url, sort_order)
+ VALUES ($1,$2,'catalog',$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
+ ON CONFLICT (site_slug, sku) DO UPDATE SET
+ title=EXCLUDED.title, vendor=EXCLUDED.vendor, product_type=EXCLUDED.product_type,
+ handle=EXCLUDED.handle, image_url=EXCLUDED.image_url, tags=EXCLUDED.tags,
+ aesthetic=EXCLUDED.aesthetic, max_price=EXCLUDED.max_price,
+ product_url=EXCLUDED.product_url, visible=true, updated_at=now()
+ `, [SITE_SLUG, skuKey, p.title, p.vendor, p.product_type, p.handle,
+ p.image_url, tags, aesthetic, p.retail_price || p.price || null,
+ p.handle ? `https://designerwallcoverings.com/products/${p.handle}` : null, i]);
+ inserted++;
+ }
+
+ const { rows: cnt } = await pool.query(
+ 'SELECT count(*) FROM microsite_products WHERE site_slug=$1', [SITE_SLUG]);
+ console.log(`candidates=${rows.length} picked=${picked.length} inserted=${inserted} total_now=${cnt[0].count}`);
+ await pool.end();
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/server.js b/server.js
index b513ec6..4c78b85 100644
--- a/server.js
+++ b/server.js
@@ -32,8 +32,10 @@ function isJunk(p) {
let PRODUCTS = [];
let DROPPED = 0;
-// graphics-loop pass 12: niche keyword filter — only show products that fit this site's niche
-const NICHE_POS = ["mod","op art","op-art","pop","geometric","psychedelic","1960"];
+// graphics-loop pass 12: niche keyword filter — only show products that fit this
+// site's niche. Substring-matched against title + tags (lowercased), so "mod"
+// catches "modern"/"mid-century modern" and "pop" catches "Pop Art"/"Pop Flower".
+const NICHE_POS = ["mod","op art","op-art","pop","geometric","psychedelic","retro","1960","mid-century","mid century","space age"];
const NICHE_NEG = ["solid","blank","plain"];
function nicheFit(p) {
const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
diff --git a/site.config.json b/site.config.json
index 67a0cbc..edea586 100644
--- a/site.config.json
+++ b/site.config.json
@@ -10,12 +10,12 @@
"accent": "#ff5cd0"
},
"rails": [
- "mod",
- "pop-art",
- "op-art",
+ "modern",
+ "pop",
+ "psychedelic",
"floral",
"geometric",
- "psychedelic"
+ "retro"
],
"port": 9839
}
← ac5bb04 stage2: wire microsite catalog + admin CRUD + grid/list view
·
back to 1960swallpaper
·
Phase 2: mobile-ready + tap-to-reveal fba9159 →