[object Object]

← back to Wallsandfabrics

stage2: wire wallsandfabrics onto shared catalog (hand-tailored)

23455867d28c9b341010f6b64b87ef6c18908b76 · 2026-05-19 11:07:54 -0700 · Steve

- catalog read-path: require _shared/admin-catalog, resolve slug from
  site.config.json (slug=wallsandfabrics), async getProducts() at startup
- preserves the surface model: PG rows run through normalize() -> isJunk()
  -> surfaceOf() exactly as before; SURFACE_COUNTS rebuilt per load
- catalog.mount() adds /admin/catalog (basic-auth) + /api/admin/* before listen
- JSON fallback retained; also falls back when PG rows lack image_url
  (Stage-2 Phase A migration missed this site's images[] array) so the
  storefront still renders real images until microsite_products is backfilled

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files touched

Diff

commit 23455867d28c9b341010f6b64b87ef6c18908b76
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 19 11:07:54 2026 -0700

    stage2: wire wallsandfabrics onto shared catalog (hand-tailored)
    
    - catalog read-path: require _shared/admin-catalog, resolve slug from
      site.config.json (slug=wallsandfabrics), async getProducts() at startup
    - preserves the surface model: PG rows run through normalize() -> isJunk()
      -> surfaceOf() exactly as before; SURFACE_COUNTS rebuilt per load
    - catalog.mount() adds /admin/catalog (basic-auth) + /api/admin/* before listen
    - JSON fallback retained; also falls back when PG rows lack image_url
      (Stage-2 Phase A migration missed this site's images[] array) so the
      storefront still renders real images until microsite_products is backfilled
    
    Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---
 server.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 59 insertions(+), 17 deletions(-)

diff --git a/server.js b/server.js
index c07d2e4..3577140 100644
--- a/server.js
+++ b/server.js
@@ -12,14 +12,10 @@ const PORT = process.env.PORT || 9934;
 const DW_SHOPIFY = 'https://designerwallcoverings.com';
 const __SITE = path.basename(__dirname);
 
-let DATA_RAW;
-try {
-  DATA_RAW = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
-  if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
-} catch (e) {
-  console.error(`[${__SITE}] FATAL: could not load products.json — ${e.message}`);
-  DATA_RAW = [];
-}
+const catalog = require('../_shared/admin-catalog');
+const siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'site.config.json'), 'utf8'));
+const SITE_SLUG = siteCfg.slug || __SITE;
+const SITE_RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
 
 // Normalize the multiple shapes that ship across DW family datasets:
 //   - cork shape: image_url + max_price + product_url
@@ -47,9 +43,6 @@ function isJunk(p) {
   return false;
 }
 
-const PRODUCTS = DATA_RAW.map(normalize).filter(p => !isJunk(p));
-console.log(`[${__SITE}] Loaded ${DATA_RAW.length}, kept ${PRODUCTS.length}, dropped ${DATA_RAW.length - PRODUCTS.length}`);
-
 // Surface tagging — derive a canonical surface bucket per product.
 function surfaceOf(p) {
   const pt = String(p.product_type || '').toLowerCase();
@@ -59,10 +52,21 @@ function surfaceOf(p) {
   if (pt.includes('trim') || /trim|tassel|braid|fringe/.test(blob)) return 'trim';
   return 'wallcovering';
 }
-for (const p of PRODUCTS) p.__surface = surfaceOf(p);
 
-const SURFACE_COUNTS = PRODUCTS.reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
-console.log(`[${__SITE}] Surfaces:`, SURFACE_COUNTS);
+// Catalog now lives in dw_unified.microsite_products — loaded async at startup
+// (see bottom of file). PG down → falls back to data/products.json. The surface
+// model is preserved: rows go through normalize() → isJunk() → surfaceOf().
+let PRODUCTS = [];
+let SURFACE_COUNTS = {};
+
+function buildCatalog(raw) {
+  const kept = raw.map(normalize).filter(p => !isJunk(p));
+  for (const p of kept) p.__surface = surfaceOf(p);
+  PRODUCTS = kept;
+  SURFACE_COUNTS = PRODUCTS.reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
+  console.log(`[${__SITE}] Loaded ${raw.length}, kept ${PRODUCTS.length}, dropped ${raw.length - PRODUCTS.length}`);
+  console.log(`[${__SITE}] Surfaces:`, SURFACE_COUNTS);
+}
 
 const app = express();
 app.use(helmet({ contentSecurityPolicy: false }));
@@ -202,6 +206,44 @@ ${urls.map(u => `  <url><loc>https://wallsandfabrics.com${u}</loc><changefreq>we
   res.type('application/xml').send(xml);
 });
 
-app.listen(PORT, '127.0.0.1', () => {
-  console.log(`[${__SITE}] listening on http://127.0.0.1:${PORT}`);
-});
+// Admin catalog CRUD — /admin/catalog (basic-auth) + /api/admin/* REST.
+catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
+
+function loadJsonFallback(reason) {
+  console.error(`[${__SITE}] ${reason} — falling back to data/products.json`);
+  let raw = [];
+  try {
+    raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
+    if (!Array.isArray(raw)) throw new Error('products.json must be an array');
+  } catch (err) {
+    console.error(`[${__SITE}] FATAL: could not load products.json — ${err.message}`);
+    raw = [];
+  }
+  buildCatalog(raw);
+}
+
+// Load the catalog from dw_unified, then start listening.
+//   • PG down / 0 rows  → data/products.json fallback.
+//   • PG rows present but the surface model would drop them all (Stage-2 Phase A
+//     migrated wallsandfabrics metadata WITHOUT image_url — this site's source
+//     JSON carries images in an images[] array the column-map missed) → also
+//     fall back to JSON so the storefront still renders real product images.
+//     Once microsite_products.image_url is backfilled for this slug, the PG
+//     path takes over automatically with no code change.
+(async () => {
+  try {
+    const rows = await catalog.getProducts(SITE_SLUG);
+    if (!rows.length) throw new Error('microsite_products returned 0 rows');
+    buildCatalog(rows);
+    if (!PRODUCTS.length) {
+      loadJsonFallback(`PG returned ${rows.length} rows but the surface model kept 0 (no image_url in microsite_products)`);
+    } else {
+      console.log(`[${__SITE}] catalog source: dw_unified.microsite_products (${rows.length} rows)`);
+    }
+  } catch (e) {
+    loadJsonFallback(`PG catalog load failed (${e.message})`);
+  }
+  app.listen(PORT, '127.0.0.1', () => {
+    console.log(`[${__SITE}] listening on http://127.0.0.1:${PORT}`);
+  });
+})();

← 33f3b27 snapshot: site.config.json rails field from stage-1 catalog  ·  back to Wallsandfabrics  ·  Phase 2: fix card-render syntax error, mobile audit pass 449e634 →