← back to Carmelwallpapers
fix: strip vendor tokens from product slug (redactVendorSlug) + dual-key route resolve — closes standalone slug vendor leak
24afde416cff3a2ec8e3bca8c9ea1ac20733d957 · 2026-05-29 22:19:58 -0700 · Steve Abrams
Files touched
Diff
commit 24afde416cff3a2ec8e3bca8c9ea1ac20733d957
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri May 29 22:19:58 2026 -0700
fix: strip vendor tokens from product slug (redactVendorSlug) + dual-key route resolve — closes standalone slug vendor leak
---
server.js | 45 ++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 42 insertions(+), 3 deletions(-)
diff --git a/server.js b/server.js
index ee0fbec..74451e6 100644
--- a/server.js
+++ b/server.js
@@ -7,6 +7,38 @@ const path = require('path');
const PORT = process.env.PORT || 9832;
const DATA_PATH = path.join(__dirname, 'data', 'products.json');
+// ── Vendor-slug redactor (canonical: dw-domain-fleet shared/render.js @ee30c6a) ─
+// Vendor names leak into the product HANDLE, which surfaces in /p/<slug> hrefs.
+// redactVendorSlug strips any hyphen-delimited segment carrying a 3rd-party vendor
+// token. Used to derive each product's `slug` (the /p/ route key) + the dual-key
+// resolver below. The REAL p.handle is left intact for the main-store buy/sample
+// links (they legitimately point at designerwallcoverings.com/products/<handle>).
+// House brands carry no denylist token, so they pass through untouched. Longer
+// vendor names precede their prefixes ("lee jofa modern" before "lee jofa") so the
+// longer match strips first.
+const VENDORS_DENYLIST = [
+ 'wolf gordon', 'nina campbell', 'jeffrey stevens', 'versace',
+ 'arte international', 'lee jofa modern', 'lee jofa', 'innovations usa',
+ 'innovations', 'koroseal', 'schumacher', 'candice olson', 'thibaut',
+ 'maya romanoff', 'scalamandre', 'dedar', 'carnegie', 'cole and son',
+ 'kravet', 'clarke and clarke', 'brunschwig', 'gp j baker', 'sandberg',
+ 'designers guild', 'graham and brown', 'harlequin', 'andrew martin',
+ 'ronald redding', 'blithfield', 'coordonne', 'fentucci', 'sister parish',
+ 'phillip jeffries', 'fromental', 'westport', 'breegan', 'les ensembliers',
+ 'roger thomas', 'stacy garcia', 'de gournay', 'romo', 'farrow and ball',
+ 'colefax and fowler', 'sanderson', 'morris and co', 'osborne and little',
+];
+const VENDOR_SLUG_RES = VENDORS_DENYLIST.map(v => {
+ const s = String(v).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
+ return new RegExp('(^|-)' + s + '(?=-|$)', 'gi');
+});
+function redactVendorSlug(slug) {
+ let out = String(slug == null ? '' : slug);
+ for (let i = 0; i < VENDOR_SLUG_RES.length; i++) out = out.replace(VENDOR_SLUG_RES[i], '$1');
+ out = out.replace(/-{2,}/g, '-').replace(/^-+|-+$/g, '');
+ return out || String(slug == null ? '' : slug); // never empty (route key must be non-empty)
+}
+
// ── Shared catalog: products live in dw_unified.microsite_products ───────────
// 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
@@ -33,6 +65,10 @@ function shapeForSite(p) {
sample_url: p.sample_url || `${productUrl}#sample`,
images: Array.isArray(p.images) && p.images.length ? p.images
: (p.image_url ? [p.image_url] : []),
+ // Vendor-redacted /p/ route key. Client card templates link via p.slug (not
+ // p.handle) so vendor tokens never reach the browser URL; the REAL p.handle
+ // stays on the object for the dual-key resolver + main-store buy links.
+ slug: redactVendorSlug(p.handle),
};
}
@@ -359,7 +395,10 @@ app.get('/health', (req, res) => {
// ── Product detail ────────────────────────────────────────────────────────────
app.get('/p/:handle', (req, res) => {
- const p = PRODUCTS.find(x => x.handle === req.params.handle);
+ // Dual-key: new redacted-slug URLs AND legacy real-handle URLs both resolve.
+ const key = req.params.handle;
+ const p = PRODUCTS.find(x => x.handle === key)
+ || PRODUCTS.find(x => redactVendorSlug(x.handle) === key);
if (!p) return res.status(404).send('<h1>Not found</h1>');
const title = `${p.title} — Carmel Wallpapers | Free Memo Samples, Trade Service`;
@@ -586,7 +625,7 @@ function renderCard(p) {
div.className = 'product-card';
div.setAttribute('role','listitem');
div.innerHTML = \`
- <a href="/p/\${p.handle}" class="card-link" aria-label="\${p.title.replace(/"/g,'"')}">
+ <a href="/p/\${p.slug || p.handle}" class="card-link" aria-label="\${p.title.replace(/"/g,'"')}">
<div class="card-img-wrap">
<img src="\${p.image_url}" alt="\${p.title.replace(/"/g,'"')}" loading="lazy" class="card-img">
</div>
@@ -662,7 +701,7 @@ async function loadSliders() {
</div>
<div class="rail-track" id="\${trackId}">
\${rail.items.map(p=>\`
- <a href="/p/\${p.handle}" class="rail-card" aria-label="\${(p.title||'').replace(/"/g,'"')}">
+ <a href="/p/\${p.slug || p.handle}" class="rail-card" aria-label="\${(p.title||'').replace(/"/g,'"')}">
<div class="rail-img-wrap"><img src="\${p.image_url}" alt="\${(p.title||'').replace(/"/g,'"')}" loading="lazy" class="rail-img"></div>
<p class="rail-card-title">\${p.title}</p>
<p class="rail-card-vendor">Designer Wallcoverings</p>
← 607c2d8 scrub vendor name leaks from products.json source (568 produ
·
back to Carmelwallpapers
·
fix: strip bare '-arte' (Arte International abbrev) from pro f922bba →