[object Object]

← back to Chinoiseriewallpaper

step2 vendor-leak fix: dual-key /sample resolver + handle_display hrefs

c4fe98a42abf290a99832af77e2c63e72537de37 · 2026-06-09 13:49:32 -0700 · Steve

Files touched

Diff

commit c4fe98a42abf290a99832af77e2c63e72537de37
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jun 9 13:49:32 2026 -0700

    step2 vendor-leak fix: dual-key /sample resolver + handle_display hrefs
---
 public/index.html |  2 +-
 server.js         | 38 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 38 insertions(+), 2 deletions(-)

diff --git a/public/index.html b/public/index.html
index ff36962..c801597 100644
--- a/public/index.html
+++ b/public/index.html
@@ -461,7 +461,7 @@
   function railCard(p){
     var img = p.image_url || (p.images && p.images[0]) || '';
     var title = p.title || p.handle || '';
-    var href = p.product_url || ('/sample/' + encodeURIComponent(p.handle || p.sku || ''));
+    var href = p.product_url || ('/sample/' + encodeURIComponent(p.handle_display || p.handle || p.sku || ''));
     return '<a class="rail-card" href="' + esc(href) + '" target="_blank" rel="noopener noreferrer">' +
            (img ? '<img src="' + esc(img) + '" alt="' + esc(title) + '" loading="lazy">' : '') +
            '<span class="rail-card-label">' + esc(title) + '</span></a>';
diff --git a/server.js b/server.js
index 4a3310f..ff29d9e 100644
--- a/server.js
+++ b/server.js
@@ -288,8 +288,43 @@ app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS_PR
 app.get('/health',     (req, res) => res.json({ status: 'ok', count: PRODUCTS_PRE.length }));
 
 // ── Sample redirect ────────────────────────────────────────────────────────
+// Step 2 of the vendor handle/sku text-leak fix (2026-06-09): the client's
+// /sample/ hrefs now point at the vendor-scrubbed `handle_display` slug
+// (emitted by _shared/api-vendor-redact since Step 1), so the raw vendor-bearing
+// handle never rides in the URL bar. This route is a pass-through redirect, so it
+// is DUAL-KEY by mapping: a raw handle/sku passes through unchanged (old bookmarks
+// + buy path, byte-identical behavior); a redacted display slug resolves to the
+// RAW Shopify handle via a map built after catalog load. If two raw keys redact
+// to the same display slug, that slug is logged and left raw-only (pass-through).
+const { redactVendorText } = require('../_shared/vendor-text-redact');
+let SAMPLE_DISPLAY = new Map(); // redacted display slug -> product
+function buildSampleDisplayMap() {
+  const map = new Map();
+  const collided = new Set();
+  for (const p of PRODUCTS_PRE) {
+    for (const key of [p.handle, p.sku]) {
+      if (!key) continue;
+      const d = redactVendorText(String(key));
+      if (!d || d === key) continue;           // nothing redacted — raw pass-through already correct
+      const prev = map.get(d);
+      if (prev && prev !== p) { collided.add(d); continue; }
+      map.set(d, p);
+    }
+  }
+  for (const d of collided) {
+    map.delete(d);
+    console.warn(`[chinoiserie] /sample display-slug collision — keeping raw-only for "${d}"`);
+  }
+  SAMPLE_DISPLAY = map;
+  console.log(`[chinoiserie] /sample dual-key map: ${map.size} display slugs, ${collided.size} collisions (raw-only)`);
+}
+
 app.get('/sample/:handle', (req, res) => {
-  res.redirect(302, `${DW_SHOPIFY}/products/${encodeURIComponent(req.params.handle)}#sample`);
+  const key = req.params.handle;
+  // Raw first — any key matching a real raw handle/sku passes through unchanged (old behavior).
+  const isRaw = PRODUCTS_PRE.some(x => x.handle === key || x.sku === key);
+  const p = isRaw ? null : SAMPLE_DISPLAY.get(key); // then the redacted display form -> raw handle
+  res.redirect(302, `${DW_SHOPIFY}/products/${encodeURIComponent(p ? p.handle : key)}#sample`);
 });
 
 // ── Robots / sitemap ───────────────────────────────────────────────────────
@@ -344,6 +379,7 @@ app.use((req, res) => {
       console.log(`[chinoiserie] loaded ${PRODUCTS_PRE.length} from data/products.json (static mode)`);
     } catch (_) { PRODUCTS_PRE = []; }
   }
+  buildSampleDisplayMap(); // Step 2: redacted->raw /sample resolver keys
   app.listen(PORT, '127.0.0.1', () => {
     console.log(`[chinoiserie] Listening on http://127.0.0.1:${PORT} — ${PRODUCTS_PRE.length} chinois patterns`);
   });

← d041e41 snapshot before step2 codemod  ·  back to Chinoiseriewallpaper  ·  chinoiseriewallpaper: rails fire again — wire sliders + grid 140fac5 →