← back to Designerwallcoverings
upstream fix (TK-10039): 8 more onboard generators write specs to metafields + prose-only body (knoll/stout/maharam/rigo/stroheim/muralsource/osborne/sanderson) via shared mapper; 0 tables, originals preserved
2ac6ccdbabc39f8702a8373d0e250aef1ea29b65 · 2026-07-29 07:31:01 -0700 · Steve Abrams
Files touched
M scripts/knoll-onboard/build-payloads.mjsM scripts/maharam-onboard/build-payloads.mjsM scripts/muralsource-onboard/build-payloads.mjsM scripts/osborne-onboard/build-payloads.mjsM scripts/rigo-onboard/build-payloads.mjsM scripts/sanderson-onboard/build-payloads.mjsM scripts/stout-onboard/build-payloads.mjsM scripts/stroheim-onboard/build-payloads.mjs
Diff
commit 2ac6ccdbabc39f8702a8373d0e250aef1ea29b65
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 29 07:31:01 2026 -0700
upstream fix (TK-10039): 8 more onboard generators write specs to metafields + prose-only body (knoll/stout/maharam/rigo/stroheim/muralsource/osborne/sanderson) via shared mapper; 0 tables, originals preserved
---
scripts/knoll-onboard/build-payloads.mjs | 17 +++++++++-----
scripts/maharam-onboard/build-payloads.mjs | 19 ++++++++++------
scripts/muralsource-onboard/build-payloads.mjs | 24 ++++++++++++--------
scripts/osborne-onboard/build-payloads.mjs | 26 ++++++++++++---------
scripts/rigo-onboard/build-payloads.mjs | 20 +++++++++++------
scripts/sanderson-onboard/build-payloads.mjs | 31 +++++++++++++++-----------
scripts/stout-onboard/build-payloads.mjs | 24 ++++++++++++--------
scripts/stroheim-onboard/build-payloads.mjs | 22 +++++++++++-------
8 files changed, 113 insertions(+), 70 deletions(-)
diff --git a/scripts/knoll-onboard/build-payloads.mjs b/scripts/knoll-onboard/build-payloads.mjs
index e29bd67..b0f341f 100644
--- a/scripts/knoll-onboard/build-payloads.mjs
+++ b/scripts/knoll-onboard/build-payloads.mjs
@@ -13,6 +13,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -53,20 +54,24 @@ function repeatStr(r) {
if (!v && !h) return '';
return [h && `H ${h}`, v && `V ${v}`].filter(Boolean).join(' · ');
}
-function bodyHtml(r) {
- const rowsHtml = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
['Pattern', clean(r.pattern_name)], ['Color', r.color_name], ['Material', r.material],
['Width', r.width], ['Length', r.length], ['Repeat', repeatStr(r)],
['Finish', r.finish], ['Fire Rating', r.fire_rating],
['Manufacturer SKU', r.mfr_sku], ['Collection', /knoll\s*textiles/i.test(r.collection||'') ? '' : r.collection],
].filter(([, v]) => v != null && String(v).trim() !== '');
+}
+function bodyHtml(r) {
const pat = titleCase(clean(r.pattern_name || r.mfr_sku));
- return `<p>${pat} from Knoll Textiles — a high-performance commercial wallcovering. Priced per single roll; request a memo sample to see the material in hand.</p>`
- + `<table>${rowsHtml.map(([k, v]) => `<tr><td><strong>${k}</strong></td><td>${String(v)}</td></tr>`).join('')}</table>`;
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<p>${pat} from Knoll Textiles — a high-performance commercial wallcovering. Priced per single roll; request a memo sample to see the material in hand.</p>`;
}
function metafields(r) {
- const mf = [];
- const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '') mf.push({ namespace: ns, key, type, value: String(value) }); };
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (Material/Width/Length/Repeat/Finish/Fire Rating/etc.)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
S('custom', 'manufacturer_sku', r.mfr_sku);
S('dwc', 'manufacturer_sku', r.mfr_sku);
S('global', 'Brand', 'Knoll');
diff --git a/scripts/maharam-onboard/build-payloads.mjs b/scripts/maharam-onboard/build-payloads.mjs
index 239932a..ebd8abf 100644
--- a/scripts/maharam-onboard/build-payloads.mjs
+++ b/scripts/maharam-onboard/build-payloads.mjs
@@ -22,6 +22,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -69,19 +70,23 @@ function repeatStr(r) {
if (!v && !h) return '';
return [h && `H ${h}`, v && `V ${v}`].filter(Boolean).join(' · ');
}
-function bodyHtml(r) {
- const rowsHtml = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
['Pattern', clean(r.pattern_name)], ['Color', r.color_name], ['Material', r.material],
['Width', r.width], ['Length', r.length], ['Repeat', repeatStr(r)],
['Finish', r.finish], ['Fire Rating', r.fire_rating], ['Application', r.application],
['Manufacturer SKU', r.mfr_sku], ['Collection', RE_BRAND.test(r.collection || '') ? '' : r.collection],
- ].filter(([, v]) => v != null && String(v).trim() !== '' && !isBadWord(String(v)));
- return `<p>${patLabel(r)} from ${BRAND} — a high-performance commercial wallcovering. Request a $${SAMPLE_PRICE} memo sample to see the material in hand; <strong>call or email for a trade quote</strong> on full rolls.</p>`
- + `<table>${rowsHtml.map(([k, v]) => `<tr><td><strong>${k}</strong></td><td>${clean(String(v))}</td></tr>`).join('')}</table>`;
+ ].filter(([, v]) => v != null && String(v).trim() !== '');
+}
+function bodyHtml(r) {
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<p>${patLabel(r)} from ${BRAND} — a high-performance commercial wallcovering. Request a $${SAMPLE_PRICE} memo sample to see the material in hand; <strong>call or email for a trade quote</strong> on full rolls.</p>`;
}
function metafields(r) {
- const mf = [];
- const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !isBadWord(String(value))) mf.push({ namespace: ns, key, type, value: String(value) }); };
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (theme-rendered)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !isBadWord(String(value)) && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
S('custom', 'manufacturer_sku', r.mfr_sku);
S('dwc', 'manufacturer_sku', r.mfr_sku);
S('global', 'Brand', BRAND);
diff --git a/scripts/muralsource-onboard/build-payloads.mjs b/scripts/muralsource-onboard/build-payloads.mjs
index 752ac43..30e15b5 100644
--- a/scripts/muralsource-onboard/build-payloads.mjs
+++ b/scripts/muralsource-onboard/build-payloads.mjs
@@ -23,6 +23,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -77,22 +78,27 @@ function repeatStr(r) {
if (!v && !h) return '';
return [h && `H ${h}`, v && `V ${v}`].filter(Boolean).join(' · ');
}
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
+ ['Pattern', clean(r.pattern_name)], ['Color', r.color_name], ['Material', r.material],
+ ['Width', r.width], ['Length', r.length], ['Repeat', repeatStr(r)],
+ ['Finish', r.finish], ['Fire Rating', r.fire_rating], ['Application', r.application],
+ ['Manufacturer SKU', r.mfr_sku], ['Collection', new RegExp(BRAND, 'i').test(r.collection || '') ? '' : r.collection],
+ ].filter(([, v]) => v != null && String(v).trim() !== '');
+}
function bodyHtml(r) {
let pat = titleCase(clean(r.pattern_name)); if (isBadWord(pat)) pat = titleCase(clean(r.mfr_sku));
const intro = (r.ai_description && String(r.ai_description).trim().length >= 24)
? clean(r.ai_description)
: `${pat} from ${BRAND} — a made-to-order scenic wallcovering mural. Request a $4.25 memo sample to see the material in hand before ordering the full mural set.`;
- const rowsHtml = [
- ['Pattern', clean(r.pattern_name)], ['Color', r.color_name], ['Material', r.material],
- ['Width', r.width], ['Length', r.length], ['Repeat', repeatStr(r)],
- ['Finish', r.finish], ['Fire Rating', r.fire_rating], ['Application', r.application],
- ['Manufacturer SKU', r.mfr_sku], ['Collection', new RegExp(BRAND, 'i').test(r.collection || '') ? '' : r.collection],
- ].filter(([, v]) => v != null && String(v).trim() !== '' && !isBadWord(String(v)));
- return `<p>${intro}</p><table>${rowsHtml.map(([k, v]) => `<tr><td><strong>${k}</strong></td><td>${clean(String(v))}</td></tr>`).join('')}</table>`;
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<p>${intro}</p>`;
}
function metafields(r) {
- const mf = [];
- const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !isBadWord(String(value))) mf.push({ namespace: ns, key, type, value: String(value) }); };
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
S('global', 'Brand', BRAND);
S('global', 'width', r.width);
S('global', 'length', r.length);
diff --git a/scripts/osborne-onboard/build-payloads.mjs b/scripts/osborne-onboard/build-payloads.mjs
index 6e607cd..87cc0c0 100644
--- a/scripts/osborne-onboard/build-payloads.mjs
+++ b/scripts/osborne-onboard/build-payloads.mjs
@@ -19,6 +19,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -34,17 +35,19 @@ function load(file) {
return fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
}
-function bodyHtml(r, e) {
- const story = e?.designStory ? `<p>${esc(e.designStory)}</p>` : `<p>${esc(r.title_core)} by Osborne & Little.</p>`;
- const specs = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r, e) {
+ return [
['Collection', r.collection],
['Material', MATERIAL],
['Brand', 'Osborne & Little'],
['Style', e?.style ? titleCase(e.style) : null],
- ].filter(([, v]) => v);
- const trs = specs.map(([k, v]) =>
- `<tr><th style="text-align:left;padding:6px 14px 6px 0;font-weight:600;">${esc(k)}</th><td style="padding:6px 0;">${esc(v)}</td></tr>`).join('\n');
- return `<div class="osborne-product">\n${story}\n<h3>Specifications</h3>\n<table style="border-collapse:collapse;">\n${trs}\n</table>\n</div>`;
+ ].filter(([, v]) => v != null && String(v).trim() !== '');
+}
+function bodyHtml(r, e) {
+ const story = e?.designStory ? `<p>${esc(e.designStory)}</p>` : `<p>${esc(r.title_core)} by Osborne & Little.</p>`;
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<div class="osborne-product">\n${story}\n</div>`;
}
function tags(r, e) {
@@ -60,9 +63,10 @@ function tags(r, e) {
}
function metafields(r, e) {
- const mf = [];
- const S = (ns, key, value) => { if (value != null && value !== '') mf.push({ namespace: ns, key, type: 'single_line_text_field', value: String(value) }); };
- const N = (ns, key, value) => { if (value != null && value !== '' && !Number.isNaN(Number(value))) mf.push({ namespace: ns, key, type: 'number_decimal', value: String(Number(value)) }); };
+ const mf = [...specMetafields(specRows(r, e))]; // canonical spec metafields (theme renders them)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
+ const N = (ns, key, value) => { if (value != null && value !== '' && !Number.isNaN(Number(value)) && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type: 'number_decimal', value: String(Number(value)) }); have.add(ns + '.' + key); } };
S('custom', 'manufacturer_sku', r.mfr_sku);
S('custom', 'pattern_name', r.title_core);
S('custom', 'background_color', e?.backgroundColor);
@@ -74,7 +78,7 @@ function metafields(r, e) {
if (e?.enriched_at) { S('custom', 'ai_enriched_at', e.enriched_at); S('custom', 'ai_enriched_by', e.enriched_by); }
const cols = (e?.colors || []).slice(0, 3);
cols.forEach((c, i) => { S('custom', `color_${i+1}_name`, c.name); S('custom', `color_${i+1}_hex`, c.hex); N('custom', `color_${i+1}_percentage`, c.percentage); });
- if (e?.colors?.length) mf.push({ namespace: 'custom', key: 'color_details', type: 'json', value: JSON.stringify(e.colors) });
+ if (e?.colors?.length && !have.has('custom.color_details')) { mf.push({ namespace: 'custom', key: 'color_details', type: 'json', value: JSON.stringify(e.colors) }); have.add('custom.color_details'); }
return mf;
}
diff --git a/scripts/rigo-onboard/build-payloads.mjs b/scripts/rigo-onboard/build-payloads.mjs
index 8400b42..bb52a6d 100644
--- a/scripts/rigo-onboard/build-payloads.mjs
+++ b/scripts/rigo-onboard/build-payloads.mjs
@@ -16,6 +16,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -64,22 +65,27 @@ function tags(r) {
return [...t].filter(Boolean).join(', ');
}
-function bodyHtml(r) {
- const spec = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
['Pattern', r.pr_pattern_name], ['Color', titleCase(r.color_name)], ['Collection', r.pr_collection_name],
['Material', r.content || '100% Vinyl'], ['Weight', r.weight_type], ['Backing', r.backing],
['Width', r.width], ['Match', r.match_type], ['Fire Rating', r.fire_rating],
- ].filter(([, v]) => v && String(v).trim());
+ ].filter(([, v]) => v != null && String(v).trim() !== '');
+}
+
+function bodyHtml(r) {
const desc = r.ai_description ||
`${r.pr_pattern_name} is a durable commercial-grade vinyl wallcovering from the ${r.pr_collection_name} collection.`;
- return `<p>${desc} Priced by quote; order a memo sample to see the material in hand.</p>` +
- `<table>${spec.map(([k, v]) => `<tr><td><strong>${k}</strong></td><td>${String(v)}</td></tr>`).join('')}</table>`;
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<p>${desc} Priced by quote; order a memo sample to see the material in hand.</p>`;
}
function metafields(r) {
- const mf = [];
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (Pattern/Color/Material/Width/Backing/Fire Rating/etc.)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
const S = (ns, key, value, type = 'single_line_text_field') => {
- if (value != null && String(value).trim() !== '') mf.push({ namespace: ns, key, type, value: String(value) });
+ if (value != null && String(value).trim() !== '' && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); }
};
S('custom', 'manufacturer_sku', r.mfr_sku);
S('private_label', 'real_vendor_name', 'Rigo Wallcovering');
diff --git a/scripts/sanderson-onboard/build-payloads.mjs b/scripts/sanderson-onboard/build-payloads.mjs
index eca4e81..e3330e2 100644
--- a/scripts/sanderson-onboard/build-payloads.mjs
+++ b/scripts/sanderson-onboard/build-payloads.mjs
@@ -22,6 +22,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -77,12 +78,9 @@ function tags(r) {
return [...t].filter(Boolean).join(', '); // always ≥4 (base set) → satisfies 5-field ≥2 tags
}
-function bodyHtml(r) {
- const pat = titleCase(r.pattern_name) || titleCase(r.mfr_sku);
- const story = !bad(r.ai_description)
- ? `<p>${esc(clean(r.ai_description))}</p>`
- : `<p>${esc(pat)}${!bad(r.color_name) ? ' in ' + esc(titleCase(r.color_name)) : ''} — a ${BRAND} wallcovering.</p>`;
- const specRows = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
['Pattern', titleCase(r.pattern_name)], ['Color', bad(r.color_name) ? '' : titleCase(r.color_name)],
['Collection', bad(r.collection) ? '' : titleCase(r.collection)],
['Material', bad(r.material) ? '' : titleCase(r.material)],
@@ -90,15 +88,22 @@ function bodyHtml(r) {
['Pattern Repeat', bad(r.pattern_repeat) ? '' : r.pattern_repeat],
['Brand', BRAND], ['Manufacturer SKU', r.mfr_sku],
].filter(([, v]) => v != null && String(v).trim() !== '');
- return `<div class="sanderson-product">\n${story}\n<h3>Specifications</h3>\n<table style="border-collapse:collapse;">\n`
- + specRows.map(([k, v]) => `<tr><th style="text-align:left;padding:6px 14px 6px 0;font-weight:600;">${esc(k)}</th><td style="padding:6px 0;">${esc(v)}</td></tr>`).join('\n')
- + `\n</table>\n</div>`;
+}
+
+function bodyHtml(r) {
+ const pat = titleCase(r.pattern_name) || titleCase(r.mfr_sku);
+ const story = !bad(r.ai_description)
+ ? `<p>${esc(clean(r.ai_description))}</p>`
+ : `<p>${esc(pat)}${!bad(r.color_name) ? ' in ' + esc(titleCase(r.color_name)) : ''} — a ${BRAND} wallcovering.</p>`;
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<div class="sanderson-product">\n${story}\n</div>`;
}
function metafields(r) {
- const mf = [];
- const S = (ns, key, value, type = 'single_line_text_field') => { if (!bad(value)) mf.push({ namespace: ns, key, type, value: String(value).trim() }); };
- const N = (ns, key, value) => { if (value != null && value !== '' && !Number.isNaN(Number(value))) mf.push({ namespace: ns, key, type: 'number_decimal', value: String(Number(value)) }); };
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (Pattern/Color/Material/Width/etc.)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (!bad(value) && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value).trim() }); have.add(ns + '.' + key); } };
+ const N = (ns, key, value) => { if (value != null && value !== '' && !Number.isNaN(Number(value)) && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type: 'number_decimal', value: String(Number(value)) }); have.add(ns + '.' + key); } };
// DUAL manufacturer_sku (task spec: custom + dwc)
S('custom', 'manufacturer_sku', r.mfr_sku);
S('dwc', 'manufacturer_sku', r.mfr_sku);
@@ -112,7 +117,7 @@ function metafields(r) {
S('mc-facebook', 'google_product_category', GOOGLE_CAT);
const cols = arr(r.ai_colors).slice(0, 3);
cols.forEach((c, i) => { const n = c && (c.name || c.color); S('custom', `color_${i+1}_name`, n); S('custom', `color_${i+1}_hex`, c && c.hex); N('custom', `color_${i+1}_percentage`, c && (c.percentage ?? c.pct)); });
- if (arr(r.ai_colors).length) mf.push({ namespace: 'custom', key: 'color_details', type: 'json', value: JSON.stringify(arr(r.ai_colors)) });
+ if (arr(r.ai_colors).length && !have.has('custom.color_details')) mf.push({ namespace: 'custom', key: 'color_details', type: 'json', value: JSON.stringify(arr(r.ai_colors)) });
const style0 = arr(r.ai_styles)[0] || arr(r.styles)[0];
S('specs', 'style', typeof style0 === 'string' ? style0 : (style0 && style0.name));
const pat0 = arr(r.ai_patterns)[0];
diff --git a/scripts/stout-onboard/build-payloads.mjs b/scripts/stout-onboard/build-payloads.mjs
index cd8a05d..4e05c44 100644
--- a/scripts/stout-onboard/build-payloads.mjs
+++ b/scripts/stout-onboard/build-payloads.mjs
@@ -26,6 +26,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -80,22 +81,27 @@ function repeatStr(r) {
if (!v && !h) return '';
return [h && `H ${h}`, v && `V ${v}`].filter(Boolean).join(' · ');
}
-function bodyHtml(r) {
- let pat = titleCase(clean(r.pattern_name)); if (isBadWord(pat)) pat = titleCase(clean(r.mfr_sku));
- const intro = (r.ai_description && String(r.ai_description).trim().length >= 24)
- ? clean(r.ai_description)
- : `${pat} from ${BRAND} — a designer wallcovering, priced and sold by the double roll. Request a $4.25 memo sample to see the material in hand before ordering full double rolls.`;
- const rowsHtml = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
['Pattern', clean(r.pattern_name)], ['Color', r.color_name], ['Material', r.material],
['Width', r.width], ['Length', r.length], ['Repeat', repeatStr(r)],
['Finish', r.finish], ['Fire Rating', r.fire_rating], ['Application', r.application],
['Manufacturer SKU', r.mfr_sku], ['Collection', new RegExp(BRAND, 'i').test(r.collection || '') ? '' : r.collection],
].filter(([, v]) => v != null && String(v).trim() !== '' && !isBadWord(String(v)));
- return `<p>${intro}</p><table>${rowsHtml.map(([k, v]) => `<tr><td><strong>${k}</strong></td><td>${clean(String(v))}</td></tr>`).join('')}</table>`;
+}
+function bodyHtml(r) {
+ let pat = titleCase(clean(r.pattern_name)); if (isBadWord(pat)) pat = titleCase(clean(r.mfr_sku));
+ const intro = (r.ai_description && String(r.ai_description).trim().length >= 24)
+ ? clean(r.ai_description)
+ : `${pat} from ${BRAND} — a designer wallcovering, priced and sold by the double roll. Request a $4.25 memo sample to see the material in hand before ordering full double rolls.`;
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ return `<p>${intro}</p>`;
}
function metafields(r) {
- const mf = [];
- const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !isBadWord(String(value))) mf.push({ namespace: ns, key, type, value: String(value) }); };
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (Material/Width/Length/Repeat/Finish/etc.)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !isBadWord(String(value)) && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
S('global', 'Brand', BRAND);
S('global', 'width', r.width);
S('global', 'length', r.length);
diff --git a/scripts/stroheim-onboard/build-payloads.mjs b/scripts/stroheim-onboard/build-payloads.mjs
index 6734004..4bcb548 100644
--- a/scripts/stroheim-onboard/build-payloads.mjs
+++ b/scripts/stroheim-onboard/build-payloads.mjs
@@ -22,6 +22,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
+import { specMetafields } from '../_spec-to-metafields.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, 'out');
@@ -70,19 +71,24 @@ function repeatStr(r) {
if (!v && !h) return '';
return [h && `H ${h}`, v && `V ${v}`].filter(Boolean).join(' · ');
}
-function bodyHtml(r) {
- let pat = titleCase(clean(r.pattern_name)); if (isBadWord(pat)) pat = titleCase(clean(r.mfr_sku));
- const intro = `${pat} from ${BRAND} — a designer wallcovering. Priced per single roll; request a $4.25 memo sample to see the material in hand before ordering.`;
- const rowsHtml = [
+// spec rows → migrated to metafields (theme renders them); NOT baked into body_html
+function specRows(r) {
+ return [
['Pattern', clean(r.pattern_name)], ['Color', r.color_name], ['Material', r.material],
['Width', r.width], ['Repeat', repeatStr(r)], ['Book', r.book],
['Manufacturer SKU', r.mfr_sku],
- ].filter(([, v]) => v != null && String(v).trim() !== '' && !isBadWord(String(v)));
- return `<p>${intro}</p><table>${rowsHtml.map(([k, v]) => `<tr><td><strong>${k}</strong></td><td>${clean(String(v))}</td></tr>`).join('')}</table>`;
+ ].filter(([, v]) => v != null && String(v).trim() !== '');
+}
+function bodyHtml(r) {
+ let pat = titleCase(clean(r.pattern_name)); if (isBadWord(pat)) pat = titleCase(clean(r.mfr_sku));
+ // PROSE ONLY — specs live in metafields (rendered by product-description-meta.liquid), never a body table
+ const intro = `${pat} from ${BRAND} — a designer wallcovering. Priced per single roll; request a $4.25 memo sample to see the material in hand before ordering.`;
+ return `<p>${intro}</p>`;
}
function metafields(r) {
- const mf = [];
- const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !isBadWord(String(value))) mf.push({ namespace: ns, key, type, value: String(value) }); };
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (Pattern/Color/Material/Width/Repeat/Book/Mfr-SKU)
+ const have = new Set(mf.map(m => m.namespace + '.' + m.key));
+ const S = (ns, key, value, type = 'single_line_text_field') => { if (value != null && String(value).trim() !== '' && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
S('global', 'Brand', BRAND);
S('global', 'width', r.width); // REQUIRED per task
S('global', 'repeat', repeatStr(r));
← 11f2c9d upstream fix: greenland-onboard writes specs to metafields (
·
back to Designerwallcoverings
·
auto-save: 2026-07-29T07:37:00 (8 files) — data/orphan-clean 695d29e →