← back to Bleachresistantfabrics
stage2: wire bleachresistantfabrics onto shared catalog (hand-tailored)
c7101ede72dc2fbfb125d4bd6ae800c8d331a1da · 2026-05-19 11:08:24 -0700 · Steve
Files touched
M package-lock.jsonM package.jsonM server.js
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 →