← back to Designer Wallcoverings
Fix accented-title casing (Unicode \p{L}) + re-title 46 Schumacher products
ac43bae87e7463264e55bf648d773c0fd1351803 · 2026-06-11 13:09:24 -0700 · SteveStudio2
capRuns used /[A-Za-z]+/ so Swedish names (KORALLÄNG) split at accents → 'KorallÄNg'.
Switched to /\p{L}+/gu. schu-fix-titles.js re-titles the 46 already-created products with
accented pattern/color names (+ strips doubled 'Wallcoverings' from color echoes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M shopify/scripts/cadence/cadence-import.jsA shopify/scripts/schu-fix-titles.js
Diff
commit ac43bae87e7463264e55bf648d773c0fd1351803
Author: SteveStudio2 <stevestudio2@SteveStudio2s-Mac-Studio.local>
Date: Thu Jun 11 13:09:24 2026 -0700
Fix accented-title casing (Unicode \p{L}) + re-title 46 Schumacher products
capRuns used /[A-Za-z]+/ so Swedish names (KORALLÄNG) split at accents → 'KorallÄNg'.
Switched to /\p{L}+/gu. schu-fix-titles.js re-titles the 46 already-created products with
accented pattern/color names (+ strips doubled 'Wallcoverings' from color echoes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
shopify/scripts/cadence/cadence-import.js | 2 +-
shopify/scripts/schu-fix-titles.js | 56 +++++++++++++++++++++++++++++++
2 files changed, 57 insertions(+), 1 deletion(-)
diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 7151a24d..7ad4e029 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -56,7 +56,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
const SMALL = new Set(['a','an','and','as','at','but','by','de','del','for','if','in','la','le','nor','of','on','or','per','the','to','und','via','vs','with']);
const KEEP_UPPER = new Set(['usa','uk','ny','nyc','sf','clj','led','pvc','uv','3d','dna']);
const decode = s => String(s||'').replace(/&/gi,'&').replace(/"/gi,'"').replace(/'|'/gi,"'").replace(/ /gi,' ');
-const capRuns = w => w.replace(/[A-Za-z]+/g, (m, off, str) => {
+const capRuns = w => w.replace(/\p{L}+/gu, (m, off, str) => {
const prev = str[off - 1];
if ((prev === "'" || prev === '’') && m.length <= 2) return m.toLowerCase(); // possessive 's / contractions
return m[0].toUpperCase() + m.slice(1).toLowerCase();
diff --git a/shopify/scripts/schu-fix-titles.js b/shopify/scripts/schu-fix-titles.js
new file mode 100644
index 00000000..a0d540e3
--- /dev/null
+++ b/shopify/scripts/schu-fix-titles.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * Fix mangled accented titles on already-created Schumacher DRAFT products.
+ *
+ * cadence-import's old capRuns used /[A-Za-z]+/ (ASCII), so Swedish names like KORALLÄNG
+ * title-cased to "KorallÄNg" (split at Ä). The importer is now fixed (\p{L}); this re-titles
+ * the products already on Shopify whose pattern/color contain accented chars.
+ * node schu-fix-titles.js # dry
+ * node schu-fix-titles.js --commit
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const { execFileSync } = require('child_process');
+const COMMIT = process.argv.includes('--commit');
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function psql(s) { return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', s], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim(); }
+
+// --- title casing (Unicode-correct) ---
+const capRuns = w => w.replace(/\p{L}+/gu, (m, off, str) => { const p = str[off - 1]; if ((p === "'" || p === '’') && m.length <= 2) return m.toLowerCase(); return m[0].toUpperCase() + m.slice(1).toLowerCase(); });
+const stripCodes = s => String(s || '').replace(/\(\s*[A-Za-z]?\d{3,}[^)]*\)/g, '').replace(/\s{2,}/g, ' ').trim();
+function titleCase(s) { return String(s || '').trim().split(/\s+/).map(capRuns).join(' '); }
+
+function gql(query, variables) {
+ return new Promise(res => { const data = JSON.stringify({ query, variables });
+ const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+ r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res(JSON.parse(d)); } catch { res({}); } }); });
+ req.on('error', () => res({})); req.write(data); req.end(); });
+}
+async function gqlRetry(q, v) { for (let a = 0; a < 5; a++) { const r = await gql(q, v); if (r.errors && JSON.stringify(r.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } await sleep(350); return r; } return {}; }
+
+(async () => {
+ if (!TOKEN) { console.error('no token'); process.exit(1); }
+ const rows = psql(`SELECT dw_sku, shopify_product_id, pattern_name, color_name FROM schumacher_catalog
+ WHERE shopify_product_id IS NOT NULL AND shopify_product_id<>''
+ AND (pattern_name ~ '[À-ſ]' OR color_name ~ '[À-ſ]');`).split('\n').filter(Boolean)
+ .map(l => { const [dw, pid, pat, col] = l.split('\t'); return { dw, pid, pat, col }; });
+ console.log(`affected products: ${rows.length}`);
+ const mUpd = `mutation($input:ProductInput!){productUpdate(input:$input){product{id title} userErrors{field message}}}`;
+ let ok = 0, fail = 0, n = 0;
+ for (const r of rows) {
+ const pattern = titleCase(stripCodes(r.pat || ''));
+ const color = r.col ? titleCase(stripCodes(r.col)) : '';
+ let core = color ? `${pattern}, ${color}` : pattern;
+ core = core.replace(/[\s,]*wallcoverings?\s*$/i, '').replace(/[\s,]*wallpapers?\s*$/i, '').trim(); // avoid double "Wallcoverings"
+ const title = `${core} Wallcoverings | Schumacher`;
+ if (n < 12) { console.log(` ${r.dw} "${r.pat}${r.col ? ', ' + r.col : ''}" → "${title}"`); n++; }
+ if (!COMMIT) continue;
+ const gid = `gid://shopify/Product/${r.pid}`;
+ const res = await gqlRetry(mUpd, { input: { id: gid, title } });
+ const ue = res?.data?.productUpdate?.userErrors;
+ if (ue && ue.length) { fail++; console.log(` ✗ ${r.dw} ${JSON.stringify(ue).slice(0, 100)}`); }
+ else { ok++; if (ok % 10 === 0) process.stdout.write(`\r fixed ${ok}/${rows.length}…`); }
+ }
+ console.log(COMMIT ? `\nDONE: re-titled ${ok}, failed ${fail}.` : `\nDRY — re-run with --commit to apply.`);
+})();
← 7cc6bdfc Add fix-broken-rolls.js — guardrailed live-price fixer (dry-
·
back to Designer Wallcoverings
·
Add schu-activate: gated DRAFT→ACTIVE for Schumacher (price+ a49bb8e4 →