← back to Fabricut Landing
server.js
140 lines
/* Fabricut editorial vendor-landing — Designer Wallcoverings house brand.
INTERNAL DATA ONLY: catalog is dw_unified.fabricut_catalog (NOT on Shopify).
Self-contained: reads data/products.json. Links resolve to this site's OWN
internal PDPs and a memo-sample/inquiry CTA — never Shopify, never fabricut.com. */
const express = require('express');
const fs = require('fs');
const path = require('path');
const PORT = Number(process.env.PORT) || 35959; // fixed prod port (nginx proxies here); 0 env → 35959
const DATA = path.join(__dirname, 'data', 'products.json');
const app = express();
app.use(require('compression')());
// Unauthenticated health probe (deploy smoke test + uptime monitors) — BEFORE the auth gate.
app.get('/healthz', (_req, res) => res.json({ ok: true, products: SNAP.products.length, dataMtime: LAST_REFRESH }));
// PUBLIC customer-facing site — NO auth gate. (The internal twin,
// fabricut--internal.designerwallcoverings.com, is the basic-auth version.)
// Data here is the PUBLIC build (MODE=public): no cost, no trade, no account #.
app.use(express.json({ limit: '2mb' }));
let SNAP = { products: [], facets: { total: 0, books: [], series: [], colors: [] } };
let LIGHT = []; // grid-index payload — heavy PDP-only fields stripped
let LAST_REFRESH = new Date().toISOString();
const REFRESH_INTERVAL_SEC = Number(process.env.REFRESH_INTERVAL_SEC || 900); // 15 min, matches the cron
function load() {
SNAP = JSON.parse(fs.readFileSync(DATA, 'utf8'));
// the index grid never renders body_html / images[] / eyebrow — keep those PDP-only
LIGHT = SNAP.products.map(({ body_html, images, display_eyebrow, published_at, ...p }) => p);
try { LAST_REFRESH = fs.statSync(DATA).mtime.toISOString(); } catch { LAST_REFRESH = new Date().toISOString(); }
console.log(`[fabricut] loaded ${SNAP.products.length} products (data mtime ${LAST_REFRESH})`);
}
load();
// Hot-reload when the 15-min cron rewrites data/products.json — no restart needed.
fs.watchFile(DATA, { interval: 5000 }, (cur, prev) => {
if (cur.mtimeMs !== prev.mtimeMs) { try { load(); } catch (e) { console.error('[fabricut] reload failed:', e.message); } }
});
// House identity — Designer Wallcoverings is the brand; Fabricut is the featured line.
const CONFIG = {
house: 'Designer Wallcoverings',
houseUrl: 'https://www.designerwallcoverings.com',
nav: [
{ label: 'The Collection', href: '#collections' },
{ label: 'Designer Wallcoverings', href: 'https://www.designerwallcoverings.com' },
],
vendor: 'Fabricut',
line: 'Fabricut',
wordmark: 'Designer Wallcoverings',
eyebrow: 'A Designer Wallcoverings Collection',
kicker: 'To the Trade · Sold Per Yard',
tagline: 'Fabricut wallcoverings — patterns, textures & colorways, curated by Designer Wallcoverings.',
booksHeading: 'The Fabricut Collection',
title: 'Fabricut — A Designer Wallcoverings Collection',
metaDescription: 'The Fabricut wallcovering collection at Designer Wallcoverings. To the trade — order a memo sample before specifying.',
slug: 'fabricut',
isPublic: true,
palette: null,
about: {
paragraphs: [
'Fabricut is one of the largest privately held distributors of decorative fabrics and wallcoverings in the world — an American house whose library spans traditional damasks and botanicals through contemporary textures, geometrics, and grasscloths. Every pattern is offered in a full run of colorways developed for residential and hospitality specification.',
'The Fabricut wallcovering collection is sold by the yard, in full-roll put-ups — presented here for the trade with complete specifications and memo samples before you specify.',
],
collab: 'The Fabricut collection, presented in collaboration with Designer Wallcoverings.',
},
};
// vendorMeta (phone / our account # / discount / pricing model) rides in from
// data/products.json — the 15-min refresh keeps it current from PG.
app.get('/api/config', (_req, res) => res.json({ ...CONFIG, vendorMeta: SNAP.vendor || null }));
// Refresh metadata — drives the corner countdown pill on the landing.
app.get('/api/meta', (_req, res) => res.json({
lastRefresh: LAST_REFRESH, intervalSec: REFRESH_INTERVAL_SEC,
count: SNAP.products.length, now: new Date().toISOString(),
}));
app.get('/api/products', (_req, res) => res.json({ count: LIGHT.length, products: LIGHT }));
app.get('/api/facets', (_req, res) => res.json(SNAP.facets));
app.get('/api/product/:handle', (req, res) => {
const p = SNAP.products.find(x => x.handle === req.params.handle);
if (!p) return res.status(404).json({ error: 'not found' });
res.json(p);
});
// "Pairs well with" — same/adjacent color family, different pattern (contrast of scale). Up to 6.
app.get('/api/pairs/:handle', (req, res) => {
const p = SNAP.products.find(x => x.handle === req.params.handle);
if (!p) return res.status(404).json({ error: 'not found' });
const hueDist = (a, b) => { if (a == null || b == null) return 180; const d = Math.abs(a - b) % 360; return d > 180 ? 360 - d : d; };
const scored = SNAP.products.filter(x => x.handle !== p.handle && x.series !== p.series).map(x => {
let s = 0;
if (x.color_bucket && x.color_bucket === p.color_bucket) s += 40;
s += Math.max(0, 30 - hueDist(x.hue, p.hue) / 2);
s += 20; // always a different pattern (filtered above) — reward scale contrast
if (x.book && x.book === p.book) s += 8;
return { x, s };
}).sort((a, b) => b.s - a.s);
// de-dup by series so pairs aren't 6 colorways of one pattern
const seen = new Set(), out = [];
for (const o of scored) { if (seen.has(o.x.series)) continue; seen.add(o.x.series); out.push(o.x); if (out.length === 6) break; }
res.json({ pairs: out });
});
// PUBLIC site — NO vendor-ops routes. The internal twin owns Memo/Stock/Price
// (which auto-email the vendor with our account #) and the private-label /curate
// + /api/selection. Cody red-team 2026-08-20: those must NOT exist on the public
// domain (unauthenticated vendor-email trigger + private-label disclosure).
//
// The ONLY public write is a sample-REQUEST that logs to a local file — it NEVER
// emails the vendor, never exposes our account #, and is always type=memo.
app.post('/api/inquiry', (req, res) => {
const { sku, name, email, note } = req.body || {};
if (!sku || !email) return res.status(400).json({ ok: false, error: 'sku and email required' });
const rec = { at: new Date().toISOString(), type: 'memo', sku, name: name || '', email, note: note || '', ip: req.ip };
try { fs.appendFileSync(path.join(__dirname, 'data', 'inquiries.jsonl'), JSON.stringify(rec) + '\n'); }
catch (e) { return res.status(500).json({ ok: false, error: 'log failed' }); }
res.json({ ok: true, message: 'Sample request received — we will be in touch.' });
});
// robots — public site indexes; internal-only routes/pages don't exist here.
app.get('/robots.txt', (_req, res) => res.type('text/plain').send('User-agent: *\nAllow: /\nSitemap: https://fabricut.designerwallcoverings.com/sitemap.xml\n'));
// Dynamic sitemap — home + every PDP, always current (rebuilt from SNAP on each request).
app.get('/sitemap.xml', (_req, res) => {
const base = 'https://fabricut.designerwallcoverings.com';
const urls = [`${base}/`, ...SNAP.products.map(p => `${base}/product/${p.handle}`)];
res.type('application/xml').send(
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`
+ urls.map(u => `<url><loc>${u}</loc></url>`).join('\n') + `\n</urlset>\n`);
});
app.use(express.static(path.join(__dirname, 'public')));
app.get('/product/:handle', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'product.html')));
const server = app.listen(PORT, () => {
console.log(`Fabricut landing → http://127.0.0.1:${server.address().port}`);
});