← back to Designerwallcoverings
upstream fix: greenland-onboard writes specs to metafields (shared _spec-to-metafields mapper) + prose-only body (no spec table)
11f2c9d9204407e4baf9c51aa34273a723c7ae2c · 2026-07-29 07:26:54 -0700 · Steve Abrams
Files touched
A scripts/_spec-to-metafields.mjsM scripts/greenland-onboard/build-payloads.mjs
Diff
commit 11f2c9d9204407e4baf9c51aa34273a723c7ae2c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 29 07:26:54 2026 -0700
upstream fix: greenland-onboard writes specs to metafields (shared _spec-to-metafields mapper) + prose-only body (no spec table)
---
scripts/_spec-to-metafields.mjs | 122 +++++++++++++++++++++++++++
scripts/greenland-onboard/build-payloads.mjs | 23 ++---
2 files changed, 134 insertions(+), 11 deletions(-)
diff --git a/scripts/_spec-to-metafields.mjs b/scripts/_spec-to-metafields.mjs
new file mode 100644
index 0000000..73a6a1b
--- /dev/null
+++ b/scripts/_spec-to-metafields.mjs
@@ -0,0 +1,122 @@
+// _spec-to-metafields.mjs — shared canonical spec-row → metafield mapper for the
+// *-onboard/build-payloads.mjs generators. Mirrors ~/dw-infinite-scroll-debug/
+// specfix-all.mjs so NEW products land specs in the exact metafields the theme
+// (snippets/product-description-meta.liquid) renders — never baked into body_html.
+//
+// Usage in a generator:
+// import { specMetafields } from '../_spec-to-metafields.mjs';
+// const rows = [['Pattern', name], ['Width', w], ...]; // [label, value] pairs
+// const mf = [...specMetafields(rows), ...otherMetafields]; // then body = prose only
+//
+// Types respected: custom.material is multi_line; product_reference custom.* keys
+// (finish/match/backing/etc.) route to their global.* / specs.* text twin.
+
+const T = {
+ pattern_name: { ns:'custom', key:'pattern_name', type:'single_line_text_field' },
+ color: { ns:'custom', key:'color', type:'single_line_text_field' },
+ material: { ns:'custom', key:'material', type:'multi_line_text_field' },
+ composition: { ns:'specs', key:'composition', type:'multi_line_text_field' },
+ width: { ns:'custom', key:'width', type:'single_line_text_field' },
+ collection_name: { ns:'custom', key:'collection_name', type:'single_line_text_field' },
+ pattern_repeat: { ns:'custom', key:'pattern_repeat', type:'single_line_text_field' },
+ fire_rating: { ns:'custom', key:'fire_rating', type:'single_line_text_field' },
+ manufacturer_sku: { ns:'custom', key:'manufacturer_sku', type:'single_line_text_field' },
+ brand: { ns:'custom', key:'brand', type:'single_line_text_field' },
+ designer: { ns:'custom', key:'designer', type:'single_line_text_field' },
+ match: { ns:'global', key:'Match', type:'single_line_text_field' },
+ finish: { ns:'global', key:'Finish', type:'single_line_text_field' },
+ backing: { ns:'global', key:'Substrate', type:'single_line_text_field' },
+ weight: { ns:'global', key:'Weight', type:'single_line_text_field' },
+ length: { ns:'custom', key:'length', type:'single_line_text_field' },
+ cleaning: { ns:'global', key:'Cleaning', type:'single_line_text_field' },
+ application: { ns:'global', key:'application', type:'single_line_text_field' },
+ origin: { ns:'global', key:'Country', type:'single_line_text_field' },
+ style: { ns:'global', key:'Style', type:'single_line_text_field' },
+ uom: { ns:'global', key:'unit_of_measure', type:'single_line_text_field' },
+ packaged: { ns:'global', key:'packaged', type:'single_line_text_field' },
+ certifications: { ns:'custom', key:'certifications', type:'single_line_text_field' },
+ grammage: { ns:'custom', key:'grammage', type:'single_line_text_field' },
+ light_fastness: { ns:'custom', key:'light_fastness', type:'single_line_text_field' },
+ washability: { ns:'custom', key:'washability', type:'single_line_text_field' },
+ removal: { ns:'custom', key:'removal', type:'single_line_text_field' },
+ adhesive: { ns:'custom', key:'adhesive', type:'single_line_text_field' },
+ coverage: { ns:'custom', key:'coverage', type:'single_line_text_field' },
+ wall_coverage: { ns:'custom', key:'wall_coverage', type:'single_line_text_field' },
+ grade: { ns:'custom', key:'grade', type:'single_line_text_field' },
+ leadtime: { ns:'custom', key:'leadtime', type:'single_line_text_field' },
+ dimensions: { ns:'custom', key:'dimensions', type:'single_line_text_field' },
+ horizontal_repeat: { ns:'custom', key:'horizontal_repeat', type:'single_line_text_field' },
+ prop65: { ns:'custom', key:'prop65', type:'single_line_text_field' },
+ catb117: { ns:'custom', key:'catb117', type:'single_line_text_field' },
+ min_order: { ns:'custom', key:'min_order', type:'single_line_text_field' },
+};
+const ALIAS = {
+ pattern:'pattern_name', 'pattern name':'pattern_name', design:'pattern_name', name:'pattern_name',
+ color:'color', colour:'color', colorway:'color', 'color name':'color',
+ material:'material', content:'material', contents:'material', 'material / content':'material', 'fabric content':'material',
+ composition:'composition',
+ width:'width', 'roll width':'width', 'fabric width':'width',
+ collection:'collection_name', 'collection name':'collection_name', book:'collection_name',
+ 'pattern repeat':'pattern_repeat', repeat:'pattern_repeat', 'vertical repeat':'pattern_repeat', 'design repeat':'pattern_repeat', 'repeat v':'pattern_repeat',
+ 'pattern match':'match', match:'match', 'match type':'match',
+ finish:'finish',
+ 'fire rating':'fire_rating', flammability:'fire_rating', flame:'fire_rating', 'flame spread':'fire_rating',
+ backing:'backing', substrate:'backing',
+ weight:'weight', 'product weight':'weight',
+ length:'length', 'roll length':'length', 'bolt length':'length', bolt:'length',
+ care:'cleaning', cleaning:'cleaning', 'clean code':'cleaning',
+ application:'application', use:'application', usage:'application',
+ origin:'origin', 'country of origin':'origin', country:'origin',
+ brand:'brand', designer:'designer', 'design by':'designer', 'designed by':'designer',
+ 'manufacturer sku':'manufacturer_sku', sku:'manufacturer_sku', 'item number':'manufacturer_sku', 'product code':'manufacturer_sku', 'pattern number':'manufacturer_sku',
+ style:'style',
+ 'unit of measure':'uom', 'sold by':'uom', 'sold per':'uom', 'priced per':'uom', uom:'uom',
+ packaging:'packaged', packaged:'packaged',
+ certifications:'certifications', certification:'certifications',
+ grammage:'grammage', gsm:'grammage',
+ 'light fastness':'light_fastness', lightfastness:'light_fastness',
+ washability:'washability', washable:'washability',
+ removal:'removal', removability:'removal',
+ adhesive:'adhesive', adhesion:'adhesive', paste:'adhesive',
+ coverage:'coverage', 'wall coverage':'wall_coverage',
+ grade:'grade',
+ 'lead time':'leadtime', 'lead-time':'leadtime', leadtime:'leadtime',
+ dimensions:'dimensions', dimension:'dimensions',
+ 'horizontal repeat':'horizontal_repeat', 'horz repeat':'horizontal_repeat',
+ 'prop 65':'prop65', prop65:'prop65', 'proposition 65':'prop65',
+ 'ca tb117':'catb117', tb117:'catb117', 'ca tb 117':'catb117',
+ 'minimum order':'min_order', 'min order':'min_order', moq:'min_order', minimum:'min_order',
+};
+// labels intentionally NOT migrated (redundant with product_type or native fields)
+const DROP = new Set(['type','product type','category','product category','pricing','price','sample size']);
+const NULLVAL = new Set(['','none','n a','na','n/a','-','--','tbd','see below']);
+
+function norm(label){
+ return String(label).toLowerCase().replace(/&/g,'&').replace(/[.:#*\/]/g,' ').replace(/\(.*?\)/g,' ').replace(/\s+/g,' ').trim();
+}
+function decodeEntities(s){
+ return String(s).replace(/ /g,' ').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/�?39;|'/g,"'").replace(/&#(\d+);/g,(m,n)=>{try{return String.fromCharCode(+n)}catch(e){return m}}).replace(/&/g,'&');
+}
+
+// rows: array of [label, value]. Returns array of {namespace,key,type,value}.
+// Skips DROP labels, blank/null values, and de-dups so two labels can't clobber
+// the same target (first wins).
+export function specMetafields(rows){
+ const out = []; const used = new Set();
+ for(const [label, rawVal] of rows){
+ if(rawVal == null) continue;
+ const val = decodeEntities(rawVal).trim();
+ if(NULLVAL.has(val.toLowerCase())) continue;
+ const n = norm(label);
+ if(DROP.has(n)) continue;
+ const ck = ALIAS[n];
+ if(!ck) continue; // unknown label → skip (won't render anyway)
+ const t = T[ck];
+ const id = t.ns + '.' + t.key;
+ if(used.has(id)) continue; // first label wins for a given target
+ used.add(id);
+ out.push({ namespace:t.ns, key:t.key, type:t.type, value:val });
+ }
+ return out;
+}
+export default specMetafields;
diff --git a/scripts/greenland-onboard/build-payloads.mjs b/scripts/greenland-onboard/build-payloads.mjs
index 2d71fa5..f53979e 100644
--- a/scripts/greenland-onboard/build-payloads.mjs
+++ b/scripts/greenland-onboard/build-payloads.mjs
@@ -9,6 +9,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');
@@ -39,28 +40,28 @@ function tags(r) {
if (r.material) t.add(titleCase(r.material));
return [...t].join(', ');
}
-function bodyHtml(r) {
- const rows = [
+// 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 || 'Cork'],
['Composition', r.composition], ['Width', r.width], ['Minimum Order', r.minimum_order],
['Repeat', r.pattern_repeat], ['Backing', r.backing], ['Fire Rating', r.fire_rating_us],
['Manufacturer SKU', r.mfr_sku], ['Collection', /tip\s*cards?/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 Greenland's natural cork collection — a warm, tactile cork wallcovering. Priced by quote; request a memo sample to see the material in hand.</p>`
- + `<table>${rows.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 Greenland's natural cork collection — a warm, tactile cork wallcovering. Priced by quote; 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) }); };
- S('custom', 'manufacturer_sku', r.mfr_sku);
+ const mf = [...specMetafields(specRows(r))]; // canonical spec metafields (Material/Composition/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() !== '' && !have.has(ns + '.' + key)) { mf.push({ namespace: ns, key, type, value: String(value) }); have.add(ns + '.' + key); } };
S('global', 'Brand', 'Greenland');
- S('global', 'width', r.width);
- S('global', 'repeat', r.pattern_repeat);
S('global', 'unit_of_measure', 'Priced Per Roll (Quote)');
- S('custom', 'color', r.color_name);
S('custom', 'real_color_name', r.color_name);
- if (r.width_inches != null) mf.push({ namespace: 'global', key: 'width_inches', type: 'number_decimal', value: String(r.width_inches) });
+ if (r.width_inches != null) S('global', 'width_inches', String(r.width_inches), 'number_decimal');
return mf;
}
← d3defbb auto-save: 2026-07-29T05:36:12 (2 files) — scripts/kravet-sp
·
back to Designerwallcoverings
·
upstream fix (TK-10039): 8 more onboard generators write spe 2ac6ccd →