← back to All Designerwallcoverings
serve every unified vendor through shared host endpoints
6032ba81468c385ea74034b7fc9061fc24330ef9 · 2026-08-28 09:17:45 -0700 · Steve Abrams
Files touched
A lib/vendor-host.jsA ops/nginx/unified-vendor-fallback.confM server.jsA test/vendor-host.test.js
Diff
commit 6032ba81468c385ea74034b7fc9061fc24330ef9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 28 09:17:45 2026 -0700
serve every unified vendor through shared host endpoints
---
lib/vendor-host.js | 29 +++++++++++++++++++
ops/nginx/unified-vendor-fallback.conf | 27 +++++++++++++++++
server.js | 53 +++++++++++++++++++++++++---------
test/vendor-host.test.js | 25 ++++++++++++++++
4 files changed, 120 insertions(+), 14 deletions(-)
diff --git a/lib/vendor-host.js b/lib/vendor-host.js
new file mode 100644
index 0000000..acef83e
--- /dev/null
+++ b/lib/vendor-host.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const { hostSlugify } = require('./vendor-pairs');
+const BASE = '.designerwallcoverings.com';
+
+function buildVendorHostMap(vendors, productRows) {
+ const map = new Map();
+ const put = (name, explicitSlug) => {
+ const vendor = String(name || '').trim();
+ const slug = hostSlugify(explicitSlug || vendor);
+ if (vendor && slug && !map.has(slug)) map.set(slug, vendor);
+ };
+ for (const row of vendors || []) put(row.name || row.vendor, row.site_slug);
+ for (const row of productRows || []) put(row.vendor);
+ return map;
+}
+
+function vendorHostContext(hostHeader, vendorMap) {
+ const host = String(hostHeader || '').split(':')[0].toLowerCase();
+ if (!host.endsWith(BASE)) return null;
+ let slug = host.slice(0, -BASE.length);
+ if (!slug || slug === 'all' || slug.includes('.')) return null;
+ const internal = slug.endsWith('-internal');
+ if (internal) slug = slug.slice(0, -'-internal'.length);
+ const vendor = vendorMap.get(slug);
+ return vendor ? { vendor, slug, internal, host } : { vendor: null, slug, internal, host };
+}
+
+module.exports = { buildVendorHostMap, vendorHostContext };
diff --git a/ops/nginx/unified-vendor-fallback.conf b/ops/nginx/unified-vendor-fallback.conf
new file mode 100644
index 0000000..f465bc4
--- /dev/null
+++ b/ops/nginx/unified-vendor-fallback.conf
@@ -0,0 +1,27 @@
+# Shared fallback for unified vendor hostnames. Existing exact server_name blocks
+# always win over this regex, so dedicated vendor applications remain untouched.
+server {
+ listen 45.61.58.125:443 ssl http2;
+ server_name ~^(?<unified_vendor>[a-z0-9-]+)\.designerwallcoverings\.com$;
+
+ ssl_certificate /etc/letsencrypt/live/designerwallcoverings.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/designerwallcoverings.com/privkey.pem;
+ include /etc/letsencrypt/options-ssl-nginx.conf;
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+
+ location / {
+ proxy_pass http://127.0.0.1:9958;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_read_timeout 120s;
+ }
+}
+
+server {
+ listen 80;
+ server_name ~^(?<unified_vendor>[a-z0-9-]+)\.designerwallcoverings\.com$;
+ return 301 https://$host$request_uri;
+}
diff --git a/server.js b/server.js
index b4eeeae..3d765f3 100644
--- a/server.js
+++ b/server.js
@@ -14,6 +14,7 @@ const { execFile } = require('child_process');
const { Pool } = require('pg');
try { require('dotenv').config(); } catch {}
const { crawlMicrosites, OUT: MICROSITES } = require('./scripts/crawl-microsites');
+const { buildVendorHostMap, vendorHostContext } = require('./lib/vendor-host');
const PORT = process.env.PORT || 9958;
const PUB = path.join(__dirname, 'public');
@@ -244,6 +245,7 @@ async function snapSql() {
}
let ROWS = [];
+let ROWS_PUBLIC_ACTIVE = [];
let ROWS_INTERNAL = []; // internal full-catalog feed (every non-archived SKU, leak-sanitized)
let MICROSITE_ROWS = []; // extra searchable rows sourced from DW-family microsite full-feeds
// (customer-facing luxury lines NOT in shopify_products — e.g. Fromental,
@@ -258,6 +260,7 @@ let STOCK = new Map(); // upper(sku) → public-safe stored stock snaps
let MFR = new Map(); // upper(bare dw_sku) → manufacturer SKU, backfilled from *_catalog tables
let VENDOR_VIEWER = new Map(); // slugify(vendor) / slug → internal line-viewer URL (the vendor's live microsite)
let VENDOR_ITEM = new Map(); // slugify(vendor) / slug → { url, handles:Set } — the microsite's FULL handle set;
+let VENDOR_HOSTS = new Map(); // canonical hostname slug → unified display vendor
// "This Item" deep-links resolve ONLY when the row's handle is IN that set.
// Line-viewer OVERRIDES (crawl-resistant). buildVendorViewer() derives the viewer map from the
@@ -895,6 +898,7 @@ async function loadSnapshot() {
// ...and every genuine catalog-only vendor line, minus the banned private-label mills.
.concat(CATALOG_ROWS.filter((r) => !BANNED.test(r.vendor || '') && !BANNED.test(r.title || ''))),
liveVendorKeys);
+ ROWS_PUBLIC_ACTIVE = ROWS.filter((r) => r.lifecycle === 'Active on site');
// INTERNAL feed — EVERY non-archived SKU (incl. private-label, NO banned-exclusion), leak-sanitized,
// plus the extra fields internal consumers (photo + substitute apps) need but the public API omits:
// product_id (numeric, for Shopify photo push), handle (for product-page URLs), tags (for material/
@@ -925,6 +929,11 @@ async function loadSnapshot() {
let fmOnly = 0;
for (const [k, fm] of FM) { if (!fm.is_discontinued && (!fm.record_type || fm.record_type === 'Master') && !FM_MATCHED.has(k)) fmOnly++; }
FM_STATS.fm_only_live = fmOnly;
+ // Host routing is derived from the same unified snapshot as the directory, with
+ // product-row vendors as a defensive fill. No hostname registry can drift separately.
+ let unifiedVendors = [];
+ try { unifiedVendors = JSON.parse(fs.readFileSync(DATA, 'utf8')).vendors || []; } catch {}
+ VENDOR_HOSTS = buildVendorHostMap(unifiedVendors, ROWS);
LOADED_AT = new Date().toISOString();
console.log(`snapshot: ${ROWS.length.toLocaleString()} public (incl. ${MICROSITE_ROWS.length.toLocaleString()} microsite + ${CATALOG_ROWS.length.toLocaleString()} catalog) + ${ROWS_INTERNAL.length.toLocaleString()} internal products in ${Date.now() - t0}ms (stock ${STOCK.size.toLocaleString()} · viewers ${VENDOR_VIEWER.size})`);
console.log(` filemaker: ${FM_STATS.rows.toLocaleString()} mirror rows · ${FM_STATS.matched.toLocaleString()} joined · ${FM_STATS.mismatch.toLocaleString()} mfr# mismatches · ${discoHidden.toLocaleString()} disco-hidden · ${fmOnly.toLocaleString()} FM-only live`);
@@ -1587,21 +1596,35 @@ const BASIC_AUTH = process.env.BASIC_AUTH
? `${process.env.BASIC_AUTH_USER}:${process.env.BASIC_AUTH_PASS}`
: 'admin:DW2024!');
const AUTH_OK = 'Basic ' + Buffer.from(BASIC_AUTH).toString('base64');
+const publicVendorPath = (p) => p === '/' || p === '/index.html' || p === '/healthz'
+ || p === '/api/products' || p === '/api/facets' || p === '/drill.js'
+ || p.startsWith('/nav-agent/');
const server = http.createServer((req, res) => {
const u = new URL(req.url, `http://localhost:${PORT}`);
+ const vendorHost = vendorHostContext(req.headers.host, VENDOR_HOSTS);
- if (u.pathname !== '/healthz' && req.headers.authorization !== AUTH_OK) {
+ // A hostname that reached the shared fallback but is not a unified vendor must
+ // never expose the all.dw catalog or its internal APIs.
+ if (vendorHost && !vendorHost.vendor) {
+ res.writeHead(404, { 'Content-Type': 'text/plain', 'X-Robots-Tag': 'noindex' });
+ return res.end('Unknown vendor endpoint');
+ }
+
+ const publicVendorRequest = vendorHost && !vendorHost.internal && publicVendorPath(u.pathname);
+ if (u.pathname !== '/healthz' && !publicVendorRequest && req.headers.authorization !== AUTH_OK) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Designer Wallcoverings - All Products"', 'Content-Type': 'text/plain' });
return res.end('Authentication required');
}
if (u.pathname === '/api/products') {
const f = parseFilters(u);
+ if (vendorHost) f.vendors = new Set([vendorHost.vendor]);
const sort = u.searchParams.get('sort') || 'newest';
const offset = parseInt(u.searchParams.get('offset') || '0', 10) || 0;
const limit = Math.min(parseInt(u.searchParams.get('limit') || '120', 10) || 120, 500);
- let rows = applyFilters(ROWS, f);
+ const catalogRows = vendorHost && !vendorHost.internal ? ROWS_PUBLIC_ACTIVE : ROWS;
+ let rows = applyFilters(catalogRows, f);
if (sorters[sort]) {
rows = [...rows].sort(sorters[sort]);
// dir=desc flips ANY sort key so every list-view column sorts both ways.
@@ -1619,7 +1642,7 @@ const server = http.createServer((req, res) => {
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
- total: rows.length, catalog: ROWS.length, loaded_at: LOADED_AT, offset, limit,
+ total: rows.length, catalog: vendorHost ? rows.length : ROWS.length, loaded_at: LOADED_AT, offset, limit,
// keep created/updated (epoch ms) so the card/list can show the active-or-staged date; only `hay` is internal
rows: rows.slice(offset, offset + limit).map(({ hay, ...r }) => r),
}));
@@ -1647,20 +1670,22 @@ const server = http.createServer((req, res) => {
if (u.pathname === '/api/facets') {
const f = parseFilters(u);
+ if (vendorHost) f.vendors = new Set([vendorHost.vendor]);
+ const facetRows = vendorHost && !vendorHost.internal ? ROWS_PUBLIC_ACTIVE : ROWS;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
- vendor: tally(applyFilters(ROWS, f, { skip: 'vendor' }), 'vendor'),
- type: tally(applyFilters(ROWS, f, { skip: 'type' }), 'type'),
- series: tally(applyFilters(ROWS, f, { skip: 'series' }), 'series'),
- lifecycle: tally(applyFilters(ROWS, f, { skip: 'lifecycle' }), 'lifecycle'),
- status: tally(applyFilters(ROWS, f, { skip: 'status' }), 'status'),
- color: tallyArr(applyFilters(ROWS, f, { skip: 'color' }), 'colors'),
- style: tallyArr(applyFilters(ROWS, f, { skip: 'style' }), 'styles'),
- material: tallyArr(applyFilters(ROWS, f, { skip: 'material' }), 'materials'),
- price_band: tally(applyFilters(ROWS, f, { skip: 'price' }), 'price_band'),
- image_state: tally(applyFilters(ROWS, f, { skip: 'image' }), 'image_state'),
+ vendor: tally(applyFilters(facetRows, f, vendorHost ? {} : { skip: 'vendor' }), 'vendor'),
+ type: tally(applyFilters(facetRows, f, { skip: 'type' }), 'type'),
+ series: tally(applyFilters(facetRows, f, { skip: 'series' }), 'series'),
+ lifecycle: tally(applyFilters(facetRows, f, { skip: 'lifecycle' }), 'lifecycle'),
+ status: tally(applyFilters(facetRows, f, { skip: 'status' }), 'status'),
+ color: tallyArr(applyFilters(facetRows, f, { skip: 'color' }), 'colors'),
+ style: tallyArr(applyFilters(facetRows, f, { skip: 'style' }), 'styles'),
+ material: tallyArr(applyFilters(facetRows, f, { skip: 'material' }), 'materials'),
+ price_band: tally(applyFilters(facetRows, f, { skip: 'price' }), 'price_band'),
+ image_state: tally(applyFilters(facetRows, f, { skip: 'image' }), 'image_state'),
price_order: PRICE_ORDER, family_order: FAMILY_ORDER, lifecycle_order: LIFE_ORDER,
- total: applyFilters(ROWS, f).length,
+ total: applyFilters(facetRows, f).length,
filemaker: FM_STATS,
}));
return;
diff --git a/test/vendor-host.test.js b/test/vendor-host.test.js
new file mode 100644
index 0000000..528027b
--- /dev/null
+++ b/test/vendor-host.test.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { buildVendorHostMap, vendorHostContext } = require('../lib/vendor-host');
+
+const hosts = buildVendorHostMap([{ name: 'Farrow & Ball' }, { name: 'Astek' }], []);
+
+test('public and internal twins resolve to the same unified vendor', () => {
+ assert.deepEqual(vendorHostContext('farrow-and-ball.designerwallcoverings.com', hosts), {
+ vendor: 'Farrow & Ball', slug: 'farrow-and-ball', internal: false,
+ host: 'farrow-and-ball.designerwallcoverings.com',
+ });
+ assert.equal(vendorHostContext('farrow-and-ball-internal.designerwallcoverings.com', hosts).vendor, 'Farrow & Ball');
+ assert.equal(vendorHostContext('farrow-and-ball-internal.designerwallcoverings.com', hosts).internal, true);
+});
+
+test('unknown wildcard hosts are explicit non-vendors', () => {
+ const ctx = vendorHostContext('not-a-real-vendor.designerwallcoverings.com', hosts);
+ assert.equal(ctx.vendor, null);
+});
+
+test('all.dw remains outside vendor fallback routing', () => {
+ assert.equal(vendorHostContext('all.designerwallcoverings.com', hosts), null);
+});
← 10fd1a2 record live vendor pair deployment proof
·
back to All Designerwallcoverings
·
probe shared vendor pairs without downloading duplicate feed 30003d2 →