[object Object]

← back to Dw Domain Fleet

shared/render.js: scrub head-baked vendor from product titles (audit 2026-05-28 task#53 addendum). scrubTags closed the tag-chip leak; titles like 'Versace 4 Metallic Wallcovering' still leaked the vendor via card-title + data-title (→ quick-view modal title + aria-label) + productPage <h1>/<title>/og:title. Add scrubTitleLead() (denylist-driven leading-vendor stripper, mirrors stripLeadVendor in _shared/api-vendor-redact.js but on THIS file's VENDORS_DENYLIST so chip+title stay on one list) + displayTitle() helper used by both card() and productPage() so the two derivations stay identical. House-brand-led names preserved; empty/generic remainder falls back to 'Designer Wallcovering'. CANARY-verified on designermagnetics.com (526 products): /catalog + /product visible-text vendor hits 0 (was: Versace in card-title+data-title), 0 empty/mangled titles, house brand preserved 67x. NOT DEPLOYED — follow-up tick fans out deploy + fleet-wide rendered-DOM re-verify per drift-aware plan.

661cbec5643bc14d921bb8a79c48b8819ce5cc2c · 2026-05-28 17:46:37 -0700 · Steve Abrams

Files touched

Diff

commit 661cbec5643bc14d921bb8a79c48b8819ce5cc2c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 28 17:46:37 2026 -0700

    shared/render.js: scrub head-baked vendor from product titles (audit 2026-05-28 task#53 addendum). scrubTags closed the tag-chip leak; titles like 'Versace 4 Metallic Wallcovering' still leaked the vendor via card-title + data-title (→ quick-view modal title + aria-label) + productPage <h1>/<title>/og:title. Add scrubTitleLead() (denylist-driven leading-vendor stripper, mirrors stripLeadVendor in _shared/api-vendor-redact.js but on THIS file's VENDORS_DENYLIST so chip+title stay on one list) + displayTitle() helper used by both card() and productPage() so the two derivations stay identical. House-brand-led names preserved; empty/generic remainder falls back to 'Designer Wallcovering'. CANARY-verified on designermagnetics.com (526 products): /catalog + /product visible-text vendor hits 0 (was: Versace in card-title+data-title), 0 empty/mangled titles, house brand preserved 67x. NOT DEPLOYED — follow-up tick fans out deploy + fleet-wide rendered-DOM re-verify per drift-aware plan.
---
 shared/render.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 47 insertions(+), 2 deletions(-)

diff --git a/shared/render.js b/shared/render.js
index 425603c..29bad9c 100644
--- a/shared/render.js
+++ b/shared/render.js
@@ -62,6 +62,51 @@ function scrubTags(tags) {
   return tags.filter(t => t && !hasVendorToken(t));
 }
 
+/* displayTitle / scrubTitleLead — strip a 3rd-party vendor baked into the HEAD
+ * of a product name before it reaches a visible surface (card-title, data-title
+ * attr → quick-view modal title + aria-label, productPage <h1>). scrubTags only
+ * touches the tag array; titles like "Versace 4 Metallic Wallcovering" or
+ * "Romo Bellisario Stripe" leaked the vendor straight through (audit 2026-05-28
+ * addendum: designermagnetics card-title "Versace …"). Mirrors stripLeadVendor in
+ * _shared/api-vendor-redact.js but is driven by THIS file's VENDORS_DENYLIST so
+ * the chip + title scrubbers stay on one denylist. Only the LEADING token is
+ * stripped (trailing "| Vendor | Collection" pipe-segments are already dropped by
+ * split('|')[0] upstream); a token mid-word ("Romono") won't match because each
+ * matcher requires a non-alphanumeric boundary after the vendor. If stripping the
+ * vendor leaves nothing meaningful, fall back to the house brand. House-brand-led
+ * names are never touched. */
+const HOUSE_BRAND = /^(hollywood|phillipe?\s*romano|retro\s*walls?|designer\s*wallcoverings?|maison\s*lustre)\b/i;
+const TITLE_GENERIC_REMAINDER = /^(wall\s*coverings?|wall\s*papers?|fabrics?|textiles?|murals?|sample|collection)?[\s\-,.|]*$/i;
+// One anchored matcher per denylist vendor, built once at module load: vendor
+// words at the very start (loose on &/and/punctuation), an optional trailing
+// noun (design/wallcovering/…), then a separator — so only a leading vendor with
+// a real word boundary after it is stripped.
+const LEAD_VENDOR_RES = VENDORS_DENYLIST.map(v =>
+  new RegExp('^' + v.replace(/[^a-z0-9]+/gi, '[^a-z0-9]+') +
+    '(?:[^a-z0-9]+(?:design|wallcoverings?|wallpapers?|fabrics?|textiles?|collection))?[\\s\\-:,|]+', 'i')
+);
+function scrubTitleLead(name) {
+  if (!name || typeof name !== 'string') return name;
+  if (HOUSE_BRAND.test(name.trim())) return name; // keep house-brand-led names
+  for (let i = 0; i < LEAD_VENDOR_RES.length; i++) {
+    const m = name.match(LEAD_VENDOR_RES[i]);
+    if (m) {
+      const rest = name.slice(m[0].length).trim();
+      if (TITLE_GENERIC_REMAINDER.test(rest) || rest.length < 4) return 'Designer Wallcovering';
+      return rest;
+    }
+  }
+  return name;
+}
+// Canonical customer-facing title derivation — leading product-name pipe-segment,
+// "wallpaper"→"wallcovering" cleaned, leading vendor stripped, with the sku/handle
+// fallback. Used by both card() and productPage() so the two stay identical.
+function displayTitle(p) {
+  const lead = clean(p && p.title ? p.title : '').split('|')[0].trim();
+  const scrubbed = scrubTitleLead(lead);
+  return scrubbed || ((p && (p.sku || p.handle) || '').replace(/-Sample$/i, '')) || 'Designer Wallcovering';
+}
+
 const DW = 'https://designerwallcoverings.com';
 
 /* ---------- shared CSS ---------- */
@@ -288,7 +333,7 @@ function jsonld(cfg) {
  */
 function card(p) {
   const hx = dominantHex(p);
-  const t = clean(p.title).split('|')[0].trim() || (p.sku||p.handle||'').replace(/-Sample$/i,'') || 'Designer Wallcovering';
+  const t = displayTitle(p);
   const href = '/product/' + encodeURIComponent(productSlug(p));
   return `<div class="card" data-handle="${esc(p.handle)}"
     data-title="${esc(t)}" data-sku="${esc(p.sku||p.handle)}"
@@ -566,7 +611,7 @@ function heroRotate(n){
  * (carry the site name + tagline), satisfying the panel SEO must-do.
  */
 function productPage(cfg, p, related) {
-  const name = clean(p.title).split('|')[0].trim() || (p.sku||p.handle||'').replace(/-Sample$/i,'') || 'Designer Wallcovering';
+  const name = displayTitle(p);
   const sku = p.sku || p.handle;
   const canonical = `${DW}/products/${encodeURIComponent(p.handle)}`;
   // per-site-unique title + meta — site name makes each fleet copy distinct

← 82e0968 shared/render.js: scrub vendor names from tag-chips fleet-wi  ·  back to Dw Domain Fleet  ·  feat(fleet pdp): theme1/theme2 toggle via ?theme=N query-par 453b000 →