← back to Glitterwalls
auto-data-snapshot: 2026-08-24T04:13:07 (1 data files) — server.js.ldrbak.1787569812
81682a1780f4b44bdfd2ffed7cc14066d47b4ae7 · 2026-08-24 04:15:00 -0700 · auto-commit-fleet
Files touched
A server.js.ldrbak.1787569812
Diff
commit 81682a1780f4b44bdfd2ffed7cc14066d47b4ae7
Author: auto-commit-fleet <steve@designerwallcoverings.com>
Date: Mon Aug 24 04:15:00 2026 -0700
auto-data-snapshot: 2026-08-24T04:13:07 (1 data files) — server.js.ldrbak.1787569812
---
server.js.ldrbak.1787569812 | 365 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 365 insertions(+)
diff --git a/server.js.ldrbak.1787569812 b/server.js.ldrbak.1787569812
new file mode 100644
index 0000000..0fecf98
--- /dev/null
+++ b/server.js.ldrbak.1787569812
@@ -0,0 +1,365 @@
+/**
+ * GLITTER WALLS — DW family vertical
+ * Curated slice from live designerwallcoverings.com Shopify catalog.
+ */
+try { require('dotenv').config({ path: require('path').join(__dirname, '.env') }); } catch (e) {}
+const express = require('express');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = process.env.PORT || 9884;
+const DW_SHOPIFY = 'https://designerwallcoverings.com';
+const __SITE = path.basename(__dirname);
+// Admin catalog (PG-backed) is opt-in via MICROSITE_ADMIN_ENABLED=true. Prod
+// stays static-only (data/products.json) so Kamatera doesn't need `pg` or the
+// _shared/ tree. Dev (Mac2) flips the flag on to use dw_unified + /admin/catalog.
+const ADMIN_ENABLED = process.env.MICROSITE_ADMIN_ENABLED === 'true';
+let catalog = null;
+if (ADMIN_ENABLED) {
+ try { catalog = require('../_shared/admin-catalog'); }
+ catch (e) { console.error(`[${__SITE}] admin-catalog unavailable (${e.message}) — falling back to static JSON`); }
+}
+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 : [];
+
+// Read-time rail membership: a product belongs to a rail if its `aesthetic` OR
+// any `tag` equals the rail key (or a declared synonym). Sister-site catalogs
+// have a degenerate `aesthetic`, so naive `p.aesthetic === key` matching
+// collapses every rail to empty — the richer signal lives in `tags`. Prefer the
+// shared matcher on dev; fall back to a self-contained copy on prod where the
+// _shared/ tree isn't deployed.
+let railMatch;
+try {
+ railMatch = require('../_shared/rail-match');
+} catch (e) {
+ const norm = s => String(s == null ? '' : s).trim().toLowerCase();
+ const productInRail = (p, key, syn) => {
+ const vals = [norm(key)].concat((syn && Array.isArray(syn[key]) ? syn[key] : []).map(norm));
+ if (vals.includes(norm(p && p.aesthetic))) return true;
+ const tags = p && Array.isArray(p.tags) ? p.tags : [];
+ return tags.some(t => vals.includes(norm(t)));
+ };
+ railMatch = {
+ productInRail,
+ buildRails(products, railKeys, opts) {
+ opts = opts || {}; const syn = opts.synonyms || null;
+ const minShare = opts.minShare != null ? opts.minShare : 0.08;
+ const minItems = opts.minItems != null ? opts.minItems : 4;
+ const perRail = opts.perRail != null ? opts.perRail : 12;
+ const maxShare = opts.maxShare != null ? opts.maxShare : 0.99;
+ const list = Array.isArray(products) ? products : []; const N = list.length || 1; const out = [];
+ for (const key of (railKeys || [])) {
+ const m = list.filter(p => productInRail(p, key, syn)); const c = m.length;
+ if (c >= minItems && c / N >= minShare && c / N <= maxShare) out.push({ key, aesthetic: key, count: c, items: m.slice(0, perRail) });
+ }
+ return out;
+ },
+ railFacets(products, railKeys, syn) {
+ const list = Array.isArray(products) ? products : []; const f = {};
+ for (const key of (railKeys || [])) f[key] = list.filter(p => productInRail(p, key, syn || null)).length;
+ return f;
+ }
+ };
+}
+const { productInRail, buildRails, railFacets } = railMatch;
+// Synonyms come from the site's own config (prod-safe); the shared default map
+// is a dev-only convenience when _shared/ is present.
+let RAIL_SYN = siteCfg.railSynonyms || {};
+if (!siteCfg.railSynonyms) { try { RAIL_SYN = require('../_shared/rail-synonyms.json'); } catch (e) {} }
+
+function isJunk(p) {
+ if (!p.image_url || !p.image_url.trim()) return true;
+ if (!p.handle && !p.sku) return true;
+ const t = p.title || '';
+ if (/lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i.test(t)) return true;
+ if (/visual.{0,3}merchandiser/i.test(t)) return true;
+ if (/(?:^|\W)image[ _-]?4(?:\W|$)/i.test(t)) return true;
+ if (/bh.?90210|beverly.?hills.?90210|iconic.{0,4}bh/i.test(t)) return true;
+ return false;
+}
+
+// Catalog now lives in dw_unified.microsite_products — populated async at
+// startup (see bottom of file). Falls back to data/products.json if PG is down.
+let PRODUCTS = [];
+let DROPPED = 0;
+
+// Glitterwalls scope (locked 2026-05-08):
+// 1) DW SKU series GLS / GLM, OR
+// 2) any product whose title/tags include "sequin" or "glitter".
+// Both rules are enforced server-side so a future re-pull of products.json
+// can't accidentally widen the storefront beyond this niche.
+function isGlsGlmSku(p) {
+ const candidates = [p.sku, p.handle].filter(Boolean);
+ for (const c of candidates) {
+ const s = String(c).toLowerCase();
+ if (/(^|[^a-z])(gls|glm)([^a-z]|$)/.test(s)) return true;
+ if (/^(gls|glm)[\d.\-_]/.test(s)) return true;
+ }
+ return false;
+}
+function hasSequinOrGlitter(p) {
+ const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
+ return /\bsequin/.test(blob) || /\bglitter/.test(blob);
+}
+function nicheFit(p) {
+ return isGlsGlmSku(p) || hasSequinOrGlitter(p);
+}
+// Niche-filtered list, populated at startup alongside PRODUCTS.
+let PRODUCTS_NICHE = [];
+
+
+const app = express();
+// DW STANDARD: sort by color/style/sku/title for any product-rendering site.
+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,
+ red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+ yellow:30,gold:31,mustard:32,olive:33,
+ green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
+ blue:60,navy:61,indigo:62,periwinkle:63,
+ purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
+};
+function colorRank(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
+ return 999;
+}
+const STYLE_TAGS = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','mid century','rustic','industrial','farmhouse','bohemian','boho','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
+function styleKey(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
+ return 'zzz';
+}
+// Hex color lookup for known color tags (used by light/dark + wheel sorts and chip dots)
+const COLOR_HEX = {
+ black:'#0a0a0a',charcoal:'#2a2a2a',gray:'#888',grey:'#888',silver:'#c0c0c0',
+ white:'#fff',ivory:'#fffff0',cream:'#f5e9c8',champagne:'#f0d8a8',beige:'#e7d6b6',sand:'#dccfa6',blonde:'#e8d8a0',
+ brown:'#6b4a2b',chestnut:'#5a3825',umber:'#5a3a1f',tan:'#c9a36a',khaki:'#bda464',honey:'#d6a23c',
+ copper:'#b87333',brass:'#b5a642',bronze:'#9c6b30',gold:'#d4af37',amber:'#c98a32',butterscotch:'#d6943c',saffron:'#d6a93c',ochre:'#b88c3a',mustard:'#caa53d',
+ red:'#c92a2a',coral:'#e87a6b',pink:'#e6a4b4',salmon:'#e08e7c',rose:'#d68a9a',magenta:'#c5358a',fuchsia:'#d63aaf',
+ orange:'#e07a32',peach:'#f0bb96',apricot:'#e0a373',
+ yellow:'#e8c84a',butter:'#f0e1a0',
+ olive:'#7a7a36','olive ':'#7a7a36',
+ green:'#3d8a4f',lime:'#92c84a',mint:'#aedcc1',
+ teal:'#2a8a8a',aqua:'#3ab4b4',turquoise:'#3ab8b0',cyan:'#3acac4',aquamarine:'#9ed5c8',
+ blue:'#3a5fb8',navy:'#1a2c52',indigo:'#37327a',periwinkle:'#8a93d4',
+ purple:'#6a3a8a',violet:'#7a4aa8',lavender:'#b0a3d6',plum:'#693a55',gilver:'#bfb9b3'
+};
+function rgbOfTag(tag) {
+ const k = String(tag||'').toLowerCase().trim();
+ for (const name of Object.keys(COLOR_HEX)) if (k.includes(name)) return COLOR_HEX[name];
+ return null;
+}
+function hexToRgb(h){const m=/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(h||'');return m?[parseInt(m[1],16),parseInt(m[2],16),parseInt(m[3],16)]:null;}
+function rgbToHsl(r,g,b){r/=255;g/=255;b/=255;const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h=0,s=0,l=(mx+mn)/2;if(mx!==mn){const d=mx-mn;s=l>.5?d/(2-mx-mn):d/(mx+mn);switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;case b:h=(r-g)/d+4;break;}h*=60;}return[h,s,l];}
+function dominantHex(p){
+ if (p.dominant_hex_real && /^#[\da-f]{6}$/i.test(p.dominant_hex_real)) return p.dominant_hex_real;
+ const tags=(p.tags||[]).map(t=>String(t).toLowerCase());
+ for (const t of tags){const hx=rgbOfTag(t);if(hx)return hx;}
+ return '#888';
+}
+// Up to 4 distinct color dots: real-pixel hex first, then tag-derived hexes.
+function colorDots(p){
+ const seen=new Set();const out=[];
+ if (p.dominant_hex_real && /^#[\da-f]{6}$/i.test(p.dominant_hex_real)) {
+ out.push(p.dominant_hex_real); seen.add(p.dominant_hex_real.toLowerCase());
+ }
+ for (const t of (p.tags||[])){
+ const hx=rgbOfTag(t);
+ if(hx && !seen.has(hx.toLowerCase())){seen.add(hx.toLowerCase());out.push(hx);if(out.length>=4)break;}
+ }
+ return out;
+}
+function luminance(p){const rgb=hexToRgb(dominantHex(p));return rgb?(0.2126*rgb[0]+0.7152*rgb[1]+0.0722*rgb[2]):128;}
+function hue(p){const rgb=hexToRgb(dominantHex(p));if(!rgb)return 999;const [h,s]=rgbToHsl(rgb[0],rgb[1],rgb[2]);return s<0.08?999:h;}
+
+function sortProducts(list, mode) {
+ if (mode === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+ if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'light-dark') return [...list].sort((a,b) => luminance(b) - luminance(a)); // bright first
+ if (mode === 'dark-light') return [...list].sort((a,b) => luminance(a) - luminance(b)); // dark first
+ if (mode === 'wheel') return [...list].sort((a,b) => hue(a) - hue(b));
+ if (mode === 'price-asc') return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
+ if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+ // List-view sortable columns — every column sortable in both directions.
+ if (mode === 'title-desc') return [...list].sort((a,b) => String(b.title || '').localeCompare(String(a.title || '')));
+ if (mode === 'sku-desc') return [...list].sort((a,b) => String(b.sku || b.handle || '').localeCompare(String(a.sku || a.handle || '')));
+ if (mode === 'vendor') return [...list].sort((a,b) => String(a.vendor || '').localeCompare(String(b.vendor || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'vendor-desc') return [...list].sort((a,b) => String(b.vendor || '').localeCompare(String(a.vendor || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'aesthetic') return [...list].sort((a,b) => String(a.aesthetic || '').localeCompare(String(b.aesthetic || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'aesthetic-desc') return [...list].sort((a,b) => String(b.aesthetic || '').localeCompare(String(a.aesthetic || '')) || String(a.title || '').localeCompare(String(b.title || '')));
+ return list;
+}
+
+// Decorate every product with color_dots + dominant_hex so the frontend can render.
+function decorateColors(items){return items.map(p=>({...p,color_dots:colorDots(p),dominant_hex:dominantHex(p)}));}
+
+
+app.use(require('../_shared/api-vendor-redact'));
+// 404-guard: never serve backup/pre-pass files even if they slip into public/.
+app.use((req, res, next) => {
+ if (/\.(bak)(\.|$)|\.pre-/i.test(req.path)) return res.status(404).send('Not found');
+ next();
+});
+app.use(express.json({ limit: "256kb" }));
+// Site config — prefer _shared/site-config (canonical) but fall back to an
+// inline minimal loader so this server.js is self-contained on Kamatera.
+let cfg;
+try {
+ cfg = require('../_shared/site-config').load(__dirname);
+} catch (_) {
+ const domain = siteCfg.domain || `${SITE_SLUG}.com`;
+ cfg = Object.freeze({
+ slug: SITE_SLUG, siteName: siteCfg.siteName || SITE_SLUG,
+ domain, siteEmail: siteCfg.siteEmail || `info@${domain}`,
+ contact: { siteName: siteCfg.siteName || SITE_SLUG, domain,
+ siteEmail: siteCfg.siteEmail || `info@${domain}`,
+ zdColor: (siteCfg.theme && siteCfg.theme.accent) || '#C9A14B',
+ zdPosition: siteCfg.zdPosition || 'right' },
+ auth: { siteName: SITE_SLUG },
+ });
+}
+require("./_universal-contact")(app, cfg.contact);
+require("./_universal-auth")(app, cfg.auth);
+require('../_shared/_universal-promo-banner')(app, {}); // new-arrivals promo strip (Tier B)
+app.use(express.static(path.join(__dirname, 'public')));
+
+// Clean-URL routes for editorial pages so their declared canonicals resolve.
+for (const slug of ['care', 'history', 'sourcing', 'trade']) {
+ app.get('/' + slug, (req, res) => res.sendFile(path.join(__dirname, 'public', slug + '.html')));
+}
+
+app.get('/api/products', (req, res) => {
+ const { q, aesthetic, vendor, page = 1, limit = 24, sort = 'newest'} = req.query;
+ let list = PRODUCTS_NICHE;
+ if (q) {
+ const needle = q.toLowerCase();
+ list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.description || p.body_html || '').toLowerCase().includes(needle) || (p.product_type || '').toLowerCase().includes(needle) || (p.vendor || '').toLowerCase().includes(needle) || (p.sku || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)));
+ }
+ if (aesthetic && aesthetic !== 'all') list = list.filter(p => productInRail(p, aesthetic, RAIL_SYN));
+ if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+ list = sortProducts(list, sort);
+ const total = list.length;
+ const pageNum = Math.max(1, parseInt(page) || 1);
+ const lim = Math.min(60, parseInt(limit) || 24);
+ const start = (pageNum - 1) * lim;
+ res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total/lim), sort, items: decorateColors(list.slice(start, start + lim)) });
+ // (replaced) old: pages: Math.ceil(total / lim), items: list.slice(start, start + lim) });
+});
+
+app.get('/api/sliders', (req, res) => {
+ const rails = buildRails(PRODUCTS_NICHE, SITE_RAILS, { synonyms: RAIL_SYN });
+ res.json({ rails: rails.map(r => ({ aesthetic: r.aesthetic, items: r.items })) });
+});
+
+app.get('/api/facets', (req, res) => {
+ const { q, aesthetic } = req.query;
+ let list = PRODUCTS_NICHE;
+ if (q) {
+ const needle = String(q).toLowerCase();
+ list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.description || p.body_html || '').toLowerCase().includes(needle) || (p.product_type || '').toLowerCase().includes(needle) || (p.vendor || '').toLowerCase().includes(needle) || (p.sku || '').toLowerCase().includes(needle) || (p.tags || []).some(t => String(t).toLowerCase().includes(needle)));
+ }
+ if (aesthetic && aesthetic !== 'all') list = list.filter(p => productInRail(p, aesthetic, RAIL_SYN));
+ // total reflects the FILTERED list so client counters stay accurate after a query.
+ // Aesthetic counts come from PRODUCTS_NICHE so the facet bar always shows every
+ // available bucket (otherwise picking one bucket would hide all others).
+ const aesthetics = {}; const vendors = {};
+ for (const p of PRODUCTS_NICHE) {
+ aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
+ vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
+ }
+ res.json({ aesthetics, vendors, total: list.length });
+});
+
+app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS_NICHE.length, dropped: DROPPED }));
+
+// Step 2 of the vendor handle/sku text-leak fix (2026-06-09): the client's
+// /sample/ hrefs now point at the vendor-scrubbed `handle_display` slug
+// (emitted by _shared/api-vendor-redact since Step 1), so the raw vendor-bearing
+// handle never rides in the URL bar. This route is DUAL-KEY — it resolves the raw
+// handle/sku FIRST (old bookmarks + buy path, byte-identical behavior), then falls
+// back to a redacted->product map built after catalog load. If two raw handles
+// redact to the same display slug, that slug is logged and left raw-only.
+const { redactVendorText } = require('../_shared/vendor-text-redact');
+let SAMPLE_DISPLAY = new Map(); // redacted display slug -> product
+function buildSampleDisplayMap() {
+ const map = new Map();
+ const collided = new Set();
+ for (const p of PRODUCTS_NICHE) {
+ for (const key of [p.handle, p.sku]) {
+ if (!key) continue;
+ const d = redactVendorText(String(key));
+ if (!d || d === key) continue; // nothing redacted — raw route already matches
+ const prev = map.get(d);
+ if (prev && prev !== p) { collided.add(d); continue; }
+ map.set(d, p);
+ }
+ }
+ for (const d of collided) {
+ map.delete(d);
+ console.warn(`[glitterwalls] /sample display-slug collision — keeping raw-only for "${d}"`);
+ }
+ SAMPLE_DISPLAY = map;
+ console.log(`[glitterwalls] /sample dual-key map: ${map.size} display slugs, ${collided.size} collisions (raw-only)`);
+}
+
+app.get('/sample/:handle', (req, res) => {
+ const key = req.params.handle;
+ const p = PRODUCTS_NICHE.find(x => x.handle === key || x.sku === key) // raw first — old behavior intact
+ || SAMPLE_DISPLAY.get(key); // then the redacted display form
+ if (!p) return res.status(404).send('Not found');
+ res.redirect(302, p.product_url || `${DW_SHOPIFY}/products/${encodeURIComponent(p.handle)}#sample`);
+});
+
+// /about → served by the "short history" page (no dedicated about.html exists)
+app.get('/about', (req, res) => res.sendFile(path.join(__dirname, 'public', 'history.html')));
+
+// sitemap.xml + robots.txt for SEO
+app.get('/robots.txt', (req, res) => {
+ res.type('text/plain').send(`User-agent: *
+Allow: /
+Sitemap: https://glitterwalls.com/sitemap.xml
+`);
+});
+app.get('/sitemap.xml', (req, res) => {
+ const urls = ['/'];
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+${urls.map(u => ` <url><loc>https://glitterwalls.com${u}</loc><changefreq>weekly</changefreq></url>`).join('\n')}
+</urlset>`;
+ res.type('application/xml').send(xml);
+});
+
+// Admin catalog CRUD — /admin/catalog (basic-auth) + /api/admin/* REST.
+if (catalog) catalog.mount(app, { siteSlug: SITE_SLUG, rails: SITE_RAILS });
+
+// Load the catalog. When admin is enabled, prefer dw_unified; else static JSON.
+(async () => {
+ let loaded = false;
+ if (catalog) {
+ try {
+ const rows = await catalog.getProducts(SITE_SLUG);
+ PRODUCTS = rows.filter(p => !isJunk(p));
+ DROPPED = rows.length - PRODUCTS.length;
+ console.log(`[${__SITE}] loaded ${rows.length} from dw_unified, kept ${PRODUCTS.length}, dropped ${DROPPED}`);
+ loaded = true;
+ } catch (e) {
+ console.error(`[${__SITE}] PG catalog load failed (${e.message}) — falling back to data/products.json`);
+ }
+ }
+ if (!loaded) {
+ try {
+ const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
+ PRODUCTS = (Array.isArray(raw) ? raw : []).filter(p => !isJunk(p));
+ console.log(`[${__SITE}] loaded ${PRODUCTS.length} from data/products.json (static mode)`);
+ } catch (_) { PRODUCTS = []; }
+ }
+ PRODUCTS_NICHE = PRODUCTS.filter(nicheFit);
+ console.log(`[${__SITE}] niche filter: kept ${PRODUCTS_NICHE.length} of ${PRODUCTS.length}`);
+ buildSampleDisplayMap(); // Step 2: redacted->raw /sample resolver keys
+ app.listen(PORT, '127.0.0.1', () => {
+ console.log(`glitterwalls listening on http://127.0.0.1:${PORT}`);
+ });
+})();
← a579735 Add p.visible!==false guard to product load filter (cluster5
·
back to Glitterwalls
·
TK-11341: add AdSense Auto-Ads loader + ads.txt (revert to r b41937f →