[object Object]

← back to Bleachresistantfabrics

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

c7101ede72dc2fbfb125d4bd6ae800c8d331a1da · 2026-05-19 11:08:24 -0700 · Steve

Files touched

Diff

commit c7101ede72dc2fbfb125d4bd6ae800c8d331a1da
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 19 11:08:24 2026 -0700

    stage2: wire bleachresistantfabrics onto shared catalog (hand-tailored)
---
 package-lock.json | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
 package.json      |   4 +-
 server.js         |  68 +++++++++++++++++------
 3 files changed, 215 insertions(+), 19 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 32d662b..4522e22 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,10 @@
       "name": "bleachresistantfabrics",
       "version": "0.1.0",
       "dependencies": {
+        "dotenv": "^17.4.2",
         "express": "^4.21.0",
-        "helmet": "^8.1.0"
+        "helmet": "^8.1.0",
+        "pg": "^8.13.1"
       }
     },
     "node_modules/accepts": {
@@ -172,6 +174,18 @@
         "npm": "1.2.8000 || >= 1.4.16"
       }
     },
+    "node_modules/dotenv": {
+      "version": "17.4.2",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+      "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -589,6 +603,134 @@
       "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
       "license": "MIT"
     },
+    "node_modules/pg": {
+      "version": "8.21.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
+      "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.13.0",
+        "pg-pool": "^3.14.0",
+        "pg-protocol": "^1.14.0",
+        "pg-types": "2.2.0",
+        "pgpass": "1.0.5"
+      },
+      "engines": {
+        "node": ">= 16.0.0"
+      },
+      "optionalDependencies": {
+        "pg-cloudflare": "^1.4.0"
+      },
+      "peerDependencies": {
+        "pg-native": ">=3.0.1"
+      },
+      "peerDependenciesMeta": {
+        "pg-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pg-cloudflare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
+      "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
+      "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
+      "license": "MIT"
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-pool": {
+      "version": "3.14.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
+      "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
+      "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pgpass": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.1.0"
+      }
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+      "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/proxy-addr": {
       "version": "2.0.7",
       "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -790,6 +932,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
     "node_modules/statuses": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -847,6 +998,15 @@
       "engines": {
         "node": ">= 0.8"
       }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
     }
   }
 }
diff --git a/package.json b/package.json
index 29df2c9..0b5fe43 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,9 @@
     "start": "node server.js"
   },
   "dependencies": {
+    "dotenv": "^17.4.2",
     "express": "^4.21.0",
-    "helmet": "^8.1.0"
+    "helmet": "^8.1.0",
+    "pg": "^8.13.1"
   }
 }
diff --git a/server.js b/server.js
index a6fad39..49ad49c 100644
--- a/server.js
+++ b/server.js
@@ -1,25 +1,36 @@
 /**
  * BLEACH RESISTANT FABRICS — DW family vertical
  * Hospitality + healthcare contract fabrics filtered for bleach-cleanable / Crypton /
- * vinyl-coated / performance / antimicrobial. Source catalog is dw_unified-derived
- * (data/products.json). Read-only.
+ * vinyl-coated / performance / antimicrobial. Read-only.
+ *
+ * Catalog now lives in dw_unified.microsite_products (Stage-2 Phase A migration);
+ * loaded async at startup via the shared admin-catalog module. Falls back to the
+ * static data/products.json bundled in this repo if PG is unreachable.
  */
+try { require('dotenv').config({ path: require('path').join(__dirname, '.env') }); } catch (e) {}
 const express = require('express');
 const helmet = require('helmet');
 const path = require('path');
 const fs = require('fs');
 
-const PORT = process.env.PORT || 9921;
-const DW_SHOPIFY = 'https://designerwallcoverings.com';
+const catalog = require('../_shared/admin-catalog');
+const siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'site.config.json'), 'utf8'));
 const SITE = 'bleachresistantfabrics';
+const SITE_SLUG = siteCfg.slug || SITE;          // do not assume slug === dir name
+const SITE_RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
+const PORT = process.env.PORT || siteCfg.port || 9921;
+const DW_SHOPIFY = 'https://designerwallcoverings.com';
 
-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 (e) {
-  console.error(`[${SITE}] FATAL: could not load products.json — ${e.message}`);
-  RAW = [];
+// Static-JSON fallback loader — used only if the PG read-path fails at startup.
+function loadStaticRaw() {
+  try {
+    const 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');
+    return raw;
+  } catch (e) {
+    console.error(`[${SITE}] static fallback load failed — ${e.message}`);
+    return [];
+  }
 }
 
 function isJunk(p) {
@@ -31,7 +42,6 @@ function isJunk(p) {
   if (/(?:^|\W)image[ _-]?4(?:\W|$)/i.test(t)) return true;
   return false;
 }
-const CLEAN = RAW.filter(p => !isJunk(p));
 
 // Niche filter: anything whose tags/title surface a bleach-cleanable signal.
 // This is a hospitality/healthcare-cleanability niche, so the catch-net is wide:
@@ -42,8 +52,15 @@ function nicheFit(p) {
   const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ') + ' ' + (p.product_type || '')).toLowerCase();
   return NICHE_RE.test(blob);
 }
-const PRODUCTS = CLEAN.filter(nicheFit);
-console.log(`[${SITE}] loaded ${RAW.length} → cleaned ${CLEAN.length} → niche ${PRODUCTS.length}`);
+
+// Run a raw catalog source (PG rows or static JSON) through junk + niche filters.
+function pipeline(raw) {
+  const clean = (Array.isArray(raw) ? raw : []).filter(p => !isJunk(p));
+  return clean.filter(nicheFit);
+}
+
+// Filled async at startup (see bottom of file). PG read-path → static fallback.
+let PRODUCTS = [];
 
 // Sort helpers (DW standard)
 const COLOR_RANK = {
@@ -139,6 +156,23 @@ app.get('/sitemap.xml', (req, res) => {
   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 });
+
+// Load the catalog from dw_unified, run it through the niche pipeline, then
+// start listening. PG down → fall back to the bundled data/products.json.
+(async () => {
+  try {
+    const rows = await catalog.getProducts(SITE_SLUG);
+    PRODUCTS = pipeline(rows);
+    console.log(`[${SITE}] loaded ${rows.length} from dw_unified → niche ${PRODUCTS.length}`);
+  } catch (e) {
+    console.error(`[${SITE}] PG catalog load failed (${e.message}) — falling back to data/products.json`);
+    const raw = loadStaticRaw();
+    PRODUCTS = pipeline(raw);
+    console.log(`[${SITE}] loaded ${raw.length} from static JSON → niche ${PRODUCTS.length}`);
+  }
+  app.listen(PORT, '127.0.0.1', () => {
+    console.log(`[${SITE}] listening on http://127.0.0.1:${PORT}`);
+  });
+})();

← a47fc71 embed Big Red customer chat (lower-left, retail mode, vendor  ·  back to Bleachresistantfabrics  ·  Phase 2: mobile-ready + tap-to-reveal 32fcf2f →