[object Object]

← back to Designer Wallcoverings

Scrub residual Brewster/York from 44 product descriptions (body_html leak vector)

64278b9240f5191e9e28a584b7aaa613ba1485fc · 2026-06-25 08:30:24 -0700 · Steve

Title/vendor/tag fix missed body_html — descriptions said 'collection by Brewster
& York', which also propagated into meta/og/twitter/JSON-LD. Caught by the new
dw-leak-scanner --pdp pass on its first run. Each description's brand mention
replaced with the product's own house-line vendor (LA Walls/Malibu).

Files touched

Diff

commit 64278b9240f5191e9e28a584b7aaa613ba1485fc
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jun 25 08:30:24 2026 -0700

    Scrub residual Brewster/York from 44 product descriptions (body_html leak vector)
    
    Title/vendor/tag fix missed body_html — descriptions said 'collection by Brewster
    & York', which also propagated into meta/og/twitter/JSON-LD. Caught by the new
    dw-leak-scanner --pdp pass on its first run. Each description's brand mention
    replaced with the product's own house-line vendor (LA Walls/Malibu).
---
 scripts/fix-brewster-york-descriptions.mjs | 59 ++++++++++++++++++++++++++++++
 1 file changed, 59 insertions(+)

diff --git a/scripts/fix-brewster-york-descriptions.mjs b/scripts/fix-brewster-york-descriptions.mjs
new file mode 100644
index 00000000..55a87673
--- /dev/null
+++ b/scripts/fix-brewster-york-descriptions.mjs
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+// Scrub residual Brewster/York brand mentions from product DESCRIPTIONS (body_html)
+// — the leak vector that survives a title/vendor/tag fix and propagates into the
+// meta/og/twitter/JSON-LD tags. Replaces the brand mention with the product's OWN
+// house-line vendor (LA Walls stays LA Walls; Malibu stays Malibu). Reads the
+// dw-leak-scanner PDP result for the body_html-flagged handles. DRY unless APPLY=1.
+import fs from 'node:fs';
+
+const env = Object.fromEntries(
+  fs.readFileSync(new URL('../.env', import.meta.url), 'utf8')
+    .split('\n').filter(l => l.includes('=') && !l.trim().startsWith('#'))
+    .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; })
+);
+const SHOP = env.SHOPIFY_STORE_DOMAIN, TOKEN = env.SHOPIFY_ADMIN_TOKEN, VER = env.SHOPIFY_ADMIN_API_VERSION || '2024-01';
+const APPLY = process.env.APPLY === '1';
+const SCAN = `${process.env.HOME}/.claude/skills/dw-leak-scanner/data/latest.json`;
+
+async function gql(query, variables = {}) {
+  const r = await fetch(`https://${SHOP}/admin/api/${VER}/graphql.json`, {
+    method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ query, variables }),
+  });
+  const j = await r.json();
+  if (j.errors) throw new Error(JSON.stringify(j.errors));
+  return j.data;
+}
+
+// Replace the banned brand with the product's own vendor; never touch "New York".
+function scrub(html, vendor) {
+  return String(html)
+    .replace(/Brewster\s*&(?:amp;)*\s*York/gi, vendor)        // "Brewster & York" (any &-encoding)
+    .replace(/\bBrewster\b/gi, vendor)                         // any standalone Brewster
+    .replace(/\bYork Wallcoverings?\b/gi, vendor)              // "York Wallcovering(s)"
+    .replace(/\bby York\b/gi, `by ${vendor}`);                 // "by York"
+}
+
+const scan = JSON.parse(fs.readFileSync(SCAN, 'utf8'));
+const handles = [...new Set(scan.results
+  .filter(r => r.fields_hit && r.fields_hit.body_html)
+  .map(r => r.url.split('/products/')[1]))];
+console.log(`\n=== ${APPLY ? 'APPLY' : 'DRY RUN'} — ${handles.length} description leaks (store ${SHOP}) ===\n`);
+
+let fixed = 0, skipped = 0;
+for (const h of handles) {
+  const d = await gql(`query($q:String!){products(first:1,query:$q){nodes{id vendor descriptionHtml}}}`, { q: `handle:${h}` });
+  const p = d.products.nodes[0];
+  if (!p) { console.log(`   skip ${h} — not found`); skipped++; continue; }
+  const next = scrub(p.descriptionHtml, p.vendor);
+  if (next === p.descriptionHtml) { console.log(`   noop ${h} — nothing matched`); skipped++; continue; }
+  const before = (p.descriptionHtml.match(/brewster|york/gi) || []).length;
+  const after = (next.match(/\bbrewster\b|\byork\b/gi) || []).filter(x => true).length;
+  if (!APPLY) { console.log(`   DRY  [${p.vendor}] ${h}  (brewster/york mentions ${before}→checking)`); fixed++; continue; }
+  const u = await gql(`mutation($input:ProductInput!){productUpdate(input:$input){product{id} userErrors{message}}}`,
+    { input: { id: p.id, descriptionHtml: next } });
+  const errs = u.productUpdate.userErrors;
+  console.log(errs.length ? `   ERR  ${h} :: ${JSON.stringify(errs)}` : `   ✓    [${p.vendor}] ${h}`);
+  if (!errs.length) fixed++;
+}
+console.log(`\n=== ${APPLY ? 'fixed' : 'would fix'} ${fixed}, skipped ${skipped} ===\n`);

← c3b4e551 Anna French scraper: full-page capture (all specs+cost+care+  ·  back to Designer Wallcoverings  ·  Anna French STEP2+3: local Full-Monte enrichment + interior- 785b0977 →