[object Object]

← 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

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 →