← back to Designer Wallcoverings
Fix double-encoded metafield leak: unwrap {type,value} blobs before writing custom.material / dwc.contents
b25933f1be9ea12a28f2794577b3c09a9c474234 · 2026-06-24 15:49:50 -0700 · Steve Abrams
Root cause of raw JSON ('{"type":"single_line_text_field","value":"Nonwoven"}')
appearing in the Material/Contents spec on ~4,300 DWQW/Malibu PDPs. Adds mfVal()
guard in both metafield writers so dirty source columns can no longer leak.
Files touched
M DW-Agents/vendor-command-center/server.jsM DW-Programming/nextjs-liquid-app/backfill-metafields.js
Diff
commit b25933f1be9ea12a28f2794577b3c09a9c474234
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 24 15:49:50 2026 -0700
Fix double-encoded metafield leak: unwrap {type,value} blobs before writing custom.material / dwc.contents
Root cause of raw JSON ('{"type":"single_line_text_field","value":"Nonwoven"}')
appearing in the Material/Contents spec on ~4,300 DWQW/Malibu PDPs. Adds mfVal()
guard in both metafield writers so dirty source columns can no longer leak.
---
DW-Agents/vendor-command-center/server.js | 26 ++++++++++++++++++----
.../nextjs-liquid-app/backfill-metafields.js | 19 +++++++++++++++-
2 files changed, 40 insertions(+), 5 deletions(-)
diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index eef3a09c..fd039183 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -27,6 +27,24 @@ const stdWorker = require('./standardization-worker');
const { processImage, uploadCroppedToShopify } = require('./logo-cropper');
const { validateVendor, getDiscontinuationSummary } = require('./discontinuation-validator');
+// Defensive unwrap for double-encoded metafield values. Some upstream scrapers
+// stored a whole Shopify metafield object — {"type":"single_line_text_field","value":"X"}
+// — as the *string* value of a spec column (material/composition/etc). Writing that
+// straight to a metafield leaks raw JSON onto the PDP. mfVal() returns the inner
+// .value when it detects that shape, otherwise the value unchanged. (Bug: DWQW/Malibu
+// cohort, custom.material + dwc.contents — 2026-06-24.)
+function mfVal(v) {
+ if (typeof v !== 'string') return v;
+ const s = v.trim();
+ if (s.startsWith('{') && s.includes('"value"')) {
+ try {
+ const o = JSON.parse(s);
+ if (o && typeof o.value === 'string') return o.value;
+ } catch (_) { /* not JSON — fall through */ }
+ }
+ return v;
+}
+
const app = express();
const PORT = 9660;
@@ -1103,7 +1121,7 @@ app.post('/api/launch-test', async (req, res) => {
// Private label: hide real vendor name, collection, and MFR SKU from public fields
const isPL = !!(v.is_private_label && v.private_label_name);
const tags = [displayVendor, p.collection, p.pattern_name, 'Luxury', 'Wallcovering'].filter(Boolean);
- if (p.material) tags.push(p.material);
+ if (p.material) tags.push(mfVal(p.material));
if (p.fire_rating) tags.push('Fire Rated');
if (p.design_style) tags.push(p.design_style);
// For private label, also add MFR SKU as tag (searchable but not customer-visible in title)
@@ -1188,7 +1206,7 @@ app.post('/api/launch-test', async (req, res) => {
// Private label: store real vendor info in hidden namespace
isPL ? { namespace: 'private_label', key: 'real_vendor_name', value: v.vendor_name } : null,
isPL ? { namespace: 'private_label', key: 'real_mfr_sku', value: p.mfr_sku } : null,
- { namespace: 'global', key: 'Contents', value: p.material },
+ { namespace: 'global', key: 'Contents', value: mfVal(p.material) },
{ namespace: 'global', key: 'application', value: p.application || 'Paste the wall' },
// Repeat (patterns only, not textures)
!isTexture ? { namespace: 'global', key: 'repeat', value: p.pattern_repeat } : null,
@@ -1266,7 +1284,7 @@ app.post('/api/launch-test', async (req, res) => {
if (p.pattern_name) tagSet.add(p.pattern_name);
if (p.color_name) tagSet.add(p.color_name);
if (p.collection) tagSet.add(p.collection);
- if (p.material) tagSet.add(p.material);
+ if (p.material) tagSet.add(mfVal(p.material));
// ALL colors (no "Background Color" prefix)
(aiAnalysis.allColors || []).forEach(c => { if (c) tagSet.add(c.trim()); });
@@ -1308,7 +1326,7 @@ app.post('/api/launch-test', async (req, res) => {
idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'brand', value: v.vendor_name, type: 'single_line_text_field' });
idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'manufacturer_sku', value: p.mfr_sku, type: 'single_line_text_field' });
idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'collection_name', value: p.collection || v.vendor_name, type: 'single_line_text_field' });
- idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'material', value: p.material || 'Non-woven', type: 'multi_line_text_field' });
+ idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'material', value: mfVal(p.material) || 'Non-woven', type: 'multi_line_text_field' });
// dwc.* fields
idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'pattern_name', value: p.pattern_name || '', type: 'single_line_text_field' });
idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'color', value: p.color_name || aiAnalysis.dominantColor, type: 'single_line_text_field' });
diff --git a/DW-Programming/nextjs-liquid-app/backfill-metafields.js b/DW-Programming/nextjs-liquid-app/backfill-metafields.js
index bba22616..1818e2cd 100644
--- a/DW-Programming/nextjs-liquid-app/backfill-metafields.js
+++ b/DW-Programming/nextjs-liquid-app/backfill-metafields.js
@@ -11,6 +11,23 @@ const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const pool = new Pool({ connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified') });
+// Defensive unwrap for double-encoded metafield values. Some upstream scrapers
+// stored a whole Shopify metafield object — {"type":"single_line_text_field","value":"X"}
+// — as the *string* value of a spec column (composition/material/etc). Writing that
+// straight to dwc.contents leaks raw JSON onto the PDP. Returns the inner .value when
+// it detects that shape, otherwise the value unchanged. (DWQW/Malibu cohort, 2026-06-24.)
+function mfVal(v) {
+ if (typeof v !== 'string') return v;
+ const s = v.trim();
+ if (s.startsWith('{') && s.includes('"value"')) {
+ try {
+ const o = JSON.parse(s);
+ if (o && typeof o.value === 'string') return o.value;
+ } catch (_) { /* not JSON — fall through */ }
+ }
+ return v;
+}
+
// Rate limit: 2 req/sec with burst of 40
const DELAY_MS = 550;
let requestCount = 0;
@@ -95,7 +112,7 @@ function buildMissingMetafields(product, existingKeys, vendorData) {
const vendorName = product.vendor_name || '';
const width = product.width || '';
const repeat = product.pattern_repeat || product.repeat || '';
- const composition = product.composition || '';
+ const composition = mfVal(product.composition) || '';
const maintenance = product.maintenance || product.care_instructions || '';
const country = product.country_of_origin || product.country || 'USA';
const matchType = product.match_type || '';
← 005ffea3 patcher: make .value regex non-backtracking ((?!\\w)) so re-
·
back to Designer Wallcoverings
·
Add fix-double-encoded-metafields.js: dry-run-safe repair fo bcebc432 →