[object Object]

← back to Naturaltextilewallpaper

fix: strip vendor tokens from product slug (redactVendorSlug) + dual-key route resolve — closes standalone slug vendor leak

1f0773a875ce454d6b23ad40582e96fb1ba4ff58 · 2026-05-29 22:19:31 -0700 · Steve Abrams

Files touched

Diff

commit 1f0773a875ce454d6b23ad40582e96fb1ba4ff58
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 22:19:31 2026 -0700

    fix: strip vendor tokens from product slug (redactVendorSlug) + dual-key route resolve — closes standalone slug vendor leak
---
 server.js | 46 ++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 42 insertions(+), 4 deletions(-)

diff --git a/server.js b/server.js
index 1185442..791a4da 100644
--- a/server.js
+++ b/server.js
@@ -27,6 +27,38 @@ const SITE_DOMAIN = 'naturaltextilewallpaper.com';
 const SITE_EMAIL = 'info@naturaltextilewallpaper.com';
 const GA_ID = process.env.GA_ID || '';
 
+// --- Vendor-name slug scrub (copied verbatim from dw-domain-fleet
+// shared/render.js @ ee30c6a) -------------------------------------------
+// Customer-facing surfaces must never carry a 3rd-party vendor name. The
+// product handle/sku leaks vendor tokens into the /p/<slug> href and the
+// visible SKU spec. redactVendorSlug() strips any hyphen-delimited segment
+// that is a denylisted vendor; the REAL p.handle is kept for the main-store
+// (designerwallcoverings.com/products/<real-handle>) buy link. The /p/ route
+// resolver is dual-keyed so both the redacted slug and any old real-handle
+// URL still resolve.
+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);
+}
+
 // --- Site config (slug + rails for the shared catalog module) -----------
 let siteCfg = {};
 try {
@@ -210,7 +242,11 @@ app.get('/api/products', (req, res) => {
   const pg = Math.max(1, parseInt(page)||1);
   const lim = Math.min(60, parseInt(limit)||24);
   const start = (pg-1)*lim;
-  res.json({ total, page:pg, limit:lim, pages: Math.ceil(total/lim), sort, items: list.slice(start, start+lim) });
+  // slug = vendor-scrubbed handle for the customer-facing /p/ href + visible
+  // SKU. The REAL handle is kept on the item so the main-store buy link
+  // (designerwallcoverings.com/products/<real-handle>) still resolves.
+  const items = list.slice(start, start+lim).map(p => ({ ...p, slug: redactVendorSlug(p.handle) }));
+  res.json({ total, page:pg, limit:lim, pages: Math.ceil(total/lim), sort, items });
 });
 
 // --- API: sliders (rails per material) ----------------------------------
@@ -232,7 +268,9 @@ app.get('/api/facets', (req, res) => {
 
 // --- Product detail page ------------------------------------------------
 app.get('/p/:handle', (req, res) => {
-  const p = PRODUCTS.find(x => x.handle === req.params.handle);
+  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(page404());
   const matLabel = MATERIALS.find(m=>m.key===p.material)?.label || 'Natural Textile';
   res.send(renderDetail(p, matLabel));
@@ -598,7 +636,7 @@ function renderIndex(q, material, sort) {
       if (p.max_price > 0) priceStr = '<span class="card-price">From $' + p.min_price.toFixed(2) + '</span>';
       var matLabel = ${JSON.stringify(MATERIALS.reduce((a,m)=>({...a,[m.key]:m.label}),{}))};
       var mat = matLabel[p.material] || p.material || '';
-      return '<a href="/p/'+p.handle+'" class="product-card">'
+      return '<a href="/p/'+encodeURIComponent(p.slug||p.handle)+'" class="product-card">'
         + '<div class="card-img-wrap"><img src="'+p.image_url+'" alt="'+escHtml(p.title)+'" loading="lazy" class="card-img"></div>'
         + '<div class="card-info">'
         + '<span class="card-material">'+mat+'</span>'
@@ -724,7 +762,7 @@ function renderDetail(p, matLabel) {
           <p class="spec-label">MATERIAL</p><p class="spec-value">${matLabel}</p>
           ${p.product_type ? `<p class="spec-label">TYPE</p><p class="spec-value">${escHtml(p.product_type)}</p>` : ''}
           ${p.tags && p.tags.length ? `<p class="spec-label">TAGS</p><p class="spec-value spec-tags">${p.tags.slice(0,12).map(t=>`<span class="spec-tag">${escHtml(t)}</span>`).join('')}</p>` : ''}
-          <p class="spec-label">SKU</p><p class="spec-value">${escHtml(p.sku||p.handle)}</p>
+          <p class="spec-label">SKU</p><p class="spec-value">${escHtml(redactVendorSlug(p.sku||p.handle))}</p>
         </div>
         <p class="detail-sample-note">Free memo samples ship within 2–3 business days. No commitment required. Designer Wallcoverings offers white-glove trade service for all natural-fiber installations.</p>
       </div>

← 97397c5 feat(theme): add ?theme=N toggle to headHTML() — mirrors dw-  ·  back to Naturaltextilewallpaper  ·  fix: strip bare '-arte' (Arte International abbrev) from pro b6f6b8c →