← back to Naturaltextilewallpaper
stage2: wire naturaltextilewallpaper onto shared catalog (hand-tailored)
393ab0e1fd60ea5536f7355a0060af0db4ea65c8 · 2026-05-19 11:07:43 -0700 · Steve Abrams
Files touched
M server.jsA site.config.json
Diff
commit 393ab0e1fd60ea5536f7355a0060af0db4ea65c8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 19 11:07:43 2026 -0700
stage2: wire naturaltextilewallpaper onto shared catalog (hand-tailored)
---
server.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++---------
site.config.json | 15 ++++++++++++
2 files changed, 78 insertions(+), 12 deletions(-)
diff --git a/server.js b/server.js
index 93bdf16..ee4eb0e 100644
--- a/server.js
+++ b/server.js
@@ -5,10 +5,12 @@
* Design: Loro Piana heirloom mill aesthetic — warm cream/oatmeal/charcoal, Futura-feel sans.
* Port: 9835
*/
+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 catalog = require('../_shared/admin-catalog');
const PORT = process.env.PORT || 9835;
const DW = 'https://designerwallcoverings.com';
@@ -17,15 +19,13 @@ const SITE_DOMAIN = 'naturaltextilewallpaper.com';
const SITE_EMAIL = 'info@naturaltextilewallpaper.com';
const GA_ID = process.env.GA_ID || '';
-// --- Data load ----------------------------------------------------------
-let DATA_RAW;
+// --- Site config (slug + rails for the shared catalog module) -----------
+let siteCfg = {};
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(`[NTW] FATAL: products.json load failed — ${e.message}`);
- DATA_RAW = [];
-}
+ siteCfg = JSON.parse(fs.readFileSync(path.join(__dirname, 'site.config.json'), 'utf8'));
+} catch (e) { console.error(`[NTW] site.config.json read failed — ${e.message}`); }
+const SITE_SLUG = siteCfg.slug || path.basename(__dirname);
+const SITE_RAILS = Array.isArray(siteCfg.rails) ? siteCfg.rails : [];
// --- Junk filter --------------------------------------------------------
function isJunk(p) {
@@ -40,9 +40,6 @@ function isJunk(p) {
return false;
}
-const PRODUCTS = DATA_RAW.filter(p => !isJunk(p));
-console.log(`[NTW] Loaded ${DATA_RAW.length} → kept ${PRODUCTS.length} after junk filter`);
-
// --- Material labels (the 7 filter pills) --------------------------------
const MATERIALS = [
{ key: 'linen', label: 'Linen' },
@@ -56,6 +53,42 @@ const MATERIALS = [
{ key: 'natural', label: 'Natural' },
];
+// --- Material derivation ------------------------------------------------
+// The dw_unified.microsite_products read-path does NOT carry a `material`
+// column (nor `min_price`/`images`). Derive `material` from tags/title/type,
+// and backfill `min_price`/`images` so the rest of this server is untouched.
+const MAT_KEYS = MATERIALS.map(m => m.key);
+function deriveMaterial(p) {
+ if (p.material && MAT_KEYS.includes(p.material)) return p.material;
+ const blob = ((p.title || '') + ' ' + (p.product_type || '') + ' ' +
+ (Array.isArray(p.tags) ? p.tags.join(' ') : '')).toLowerCase();
+ // most specific first; 'woven'/'natural' are catch-alls
+ for (const k of ['grasscloth','seagrass','linen','jute','raffia','sisal','hemp','bamboo','abaca']) {
+ if (blob.includes(k)) {
+ if (k === 'seagrass') return 'grasscloth';
+ if (k === 'abaca') return 'jute';
+ return k;
+ }
+ }
+ if (/\bwoven\b/.test(blob)) return 'woven';
+ return 'natural';
+}
+function normalizeProduct(p) {
+ const tags = Array.isArray(p.tags) ? p.tags
+ : (typeof p.tags === 'string' ? p.tags.split(',').map(t => t.trim()).filter(Boolean) : []);
+ const out = { ...p, tags };
+ out.material = deriveMaterial(out);
+ if (out.max_price == null && p.price != null) out.max_price = Number(p.price);
+ if (out.min_price == null) out.min_price = out.max_price != null ? Number(out.max_price) : 0;
+ if (!Array.isArray(out.images) || !out.images.length) {
+ out.images = out.image_url ? [out.image_url] : [];
+ }
+ return out;
+}
+
+// PRODUCTS is loaded async at startup from dw_unified (JSON fallback if PG down).
+let PRODUCTS = [];
+
// --- Sort ---------------------------------------------------------------
const COLOR_RANK = {
black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
@@ -545,4 +578,22 @@ function escHtml(s) {
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
-app.listen(PORT, () => console.log(`[NTW] Running on port ${PORT} — ${PRODUCTS.length} products`));
+// --- Admin catalog CRUD — /admin/catalog (basic-auth) + /api/admin/* -----
+catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
+
+// --- Async startup: load catalog from dw_unified, then listen -----------
+(async () => {
+ try {
+ const rows = await catalog.getProducts(SITE_SLUG);
+ PRODUCTS = rows.map(normalizeProduct).filter(p => !isJunk(p));
+ console.log(`[NTW] Loaded ${rows.length} from dw_unified → kept ${PRODUCTS.length} after junk filter`);
+ } catch (e) {
+ console.error(`[NTW] PG catalog load failed (${e.message}) — falling back to data/products.json`);
+ try {
+ const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
+ PRODUCTS = (Array.isArray(raw) ? raw : []).map(normalizeProduct).filter(p => !isJunk(p));
+ console.log(`[NTW] Fallback: loaded ${PRODUCTS.length} from data/products.json`);
+ } catch (_) { PRODUCTS = []; }
+ }
+ app.listen(PORT, () => console.log(`[NTW] Running on port ${PORT} — ${PRODUCTS.length} products`));
+})();
diff --git a/site.config.json b/site.config.json
new file mode 100644
index 0000000..8a4b35f
--- /dev/null
+++ b/site.config.json
@@ -0,0 +1,15 @@
+{
+ "slug": "naturaltextilewallpaper",
+ "siteName": "Naturaltextilewallpaper",
+ "domain": "naturaltextilewallpaper.com",
+ "heroHeadline": "NATURALTEXTILEWALLPAPER",
+ "rails": [
+ "commercial",
+ "vinyl",
+ "textured",
+ "architectural",
+ "fire",
+ "rated"
+ ],
+ "_generated": true
+}
← d25f742 Untrack server.js.pre-sort-skill snapshot; broaden .gitignor
·
back to Naturaltextilewallpaper
·
Phase 2: mobile-ready + tap-to-reveal 9fe6fb9 →