← back to Designer Wallcoverings
tag-designer-colors.js — FALLBACK for Boost tiers without metafield-sourced filters: writes prefixed 'color:<DesignerName>' tag per active product from custom.color_1_name (+ --top2). Prefix avoids re-polluting the cleaned color smart-collections. Idempotent/resumable. Tested 3 live, 0 failed. Run: node tag-designer-colors.js --conc 8 (only if Boost can't do metafields).
1e5c69bab6b156c62911176dd2c2622df04d4843 · 2026-06-14 14:29:38 -0700 · Steve Abrams
Files touched
A DW-Programming/tag-designer-colors.js
Diff
commit 1e5c69bab6b156c62911176dd2c2622df04d4843
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jun 14 14:29:38 2026 -0700
tag-designer-colors.js — FALLBACK for Boost tiers without metafield-sourced filters: writes prefixed 'color:<DesignerName>' tag per active product from custom.color_1_name (+ --top2). Prefix avoids re-polluting the cleaned color smart-collections. Idempotent/resumable. Tested 3 live, 0 failed. Run: node tag-designer-colors.js --conc 8 (only if Boost can't do metafields).
---
DW-Programming/tag-designer-colors.js | 80 +++++++++++++++++++++++++++++++++++
1 file changed, 80 insertions(+)
diff --git a/DW-Programming/tag-designer-colors.js b/DW-Programming/tag-designer-colors.js
new file mode 100644
index 00000000..3317b6a1
--- /dev/null
+++ b/DW-Programming/tag-designer-colors.js
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+/**
+ * FALLBACK for Boost tiers that can't source a filter from a metafield: write the
+ * designer color name as a PREFIXED tag (default "color:Ecru") on each active
+ * product, read from the custom.color_1_name (+ optionally color_2_name) metafield.
+ * Boost is then configured with a tag-based Color filter on that prefix.
+ *
+ * PREFIX is deliberate: a bare "Navy"/"Sage" tag would re-add products to the color
+ * SMART COLLECTIONS we just cleaned — the "color:" prefix avoids that collision.
+ *
+ * Idempotent + resumable: only products MISSING the tag get a productUpdate (tags
+ * are merged, never replaced wholesale beyond adding the color tag).
+ * Usage: node tag-designer-colors.js [--limit N] [--conc 6] [--top2] [--prefix "color:"] [--dry-run]
+ */
+const https = require('https');
+const args = process.argv.slice(2);
+const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API_VERSION = '2024-01';
+const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DRY = args.includes('--dry-run');
+const TOP2 = args.includes('--top2');
+const PREFIX = args.find((_, i, a) => a[i-1] === '--prefix') || 'color:';
+const LIMIT = parseInt(args.find((_, i, a) => a[i-1] === '--limit') || '0', 10) || 0;
+const CONC = parseInt(args.find((_, i, a) => a[i-1] === '--conc') || '6', 10) || 6;
+
+function gql(query, variables){
+ return new Promise((resolve, reject) => {
+ const body = JSON.stringify({ query, variables });
+ const req = https.request({ hostname: SHOPIFY_DOMAIN, path: `/admin/api/${API_VERSION}/graphql.json`, method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN, 'Content-Length': Buffer.byteLength(body) }, timeout: 30000 }, res => {
+ let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } });
+ });
+ req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); req.write(body); req.end();
+ });
+}
+const tagFor = (name) => PREFIX + String(name).trim();
+async function addTags(id, mergedTags){
+ const r = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ userErrors{message} } }`,
+ { input: { id: `gid://shopify/Product/${id}`, tags: mergedTags } });
+ const ue = r?.data?.productUpdate?.userErrors; if (ue && ue.length) throw new Error(ue[0].message);
+ if (r?.errors) throw new Error(JSON.stringify(r.errors).slice(0,100));
+}
+(async () => {
+ if (!TOKEN) { console.error('Missing SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+ console.log(`Tag designer colors — ${DRY?'DRY':'LIVE'} — prefix "${PREFIX}"${TOP2?' top2':' top1'} conc ${CONC}`);
+ let cursor=null,page=0,scanned=0,tagged=0,already=0,skip=0,failed=0; const todo=[];
+ while(true){
+ const after=cursor?`, after:"${cursor}"`:'';
+ const q=`{ products(first:100, query:"status:active"${after}){ pageInfo{hasNextPage endCursor} nodes{
+ id tags c1:metafield(namespace:"custom",key:"color_1_name"){value} c2:metafield(namespace:"custom",key:"color_2_name"){value} }}}`;
+ let r; try{r=await gql(q);}catch{ await new Promise(s=>setTimeout(s,3000)); continue; }
+ if(r.errors){ if(JSON.stringify(r.errors).includes('Throttled')){await new Promise(s=>setTimeout(s,4000));continue;} console.error(r.errors);break; }
+ const pg=r.data.products; page++;
+ for(const n of pg.nodes){ scanned++;
+ const names=[n.c1?.value].concat(TOP2?[n.c2?.value]:[]).filter(Boolean);
+ if(!names.length){ skip++; continue; }
+ const want=names.map(tagFor);
+ const have=new Set((n.tags||[]));
+ const missing=want.filter(t=>!have.has(t));
+ if(!missing.length){ already++; continue; }
+ todo.push({ id:n.id.replace('gid://shopify/Product/',''), tags:[...n.tags, ...missing] });
+ }
+ if(page%20===0) console.log(` scanned ${scanned} | to-tag ${todo.length} | already ${already} | no-color ${skip}`);
+ if(!pg.pageInfo.hasNextPage) break; cursor=pg.pageInfo.endCursor;
+ if(LIMIT && todo.length>=LIMIT) break;
+ await new Promise(s=>setTimeout(s,200));
+ }
+ const work = LIMIT ? todo.slice(0,LIMIT) : todo;
+ console.log(`\nScan: ${scanned} active | ${work.length} to tag | ${already} already tagged | ${skip} no color metafield.`);
+ let idx=0; const queue=[...work];
+ async function worker(){
+ while(queue.length){
+ const p=queue.shift(); const n=++idx;
+ try{ if(!DRY) await addTags(p.id, p.tags); tagged++; if(n<=8||n%500===0) console.log(` [${n}] tagged ${p.id}`); }
+ catch(e){ failed++; if(String(e.message).match(/Throttled|429/)) await new Promise(r=>setTimeout(r,3000)); if(n%200===0) console.log(` [${n}] ERR ${p.id}: ${e.message}`); }
+ }
+ }
+ await Promise.all(Array.from({length:CONC},worker));
+ console.log(`\nDone: ${tagged} products tagged, ${failed} failed. Boost → add a tag-based Color filter on prefix "${PREFIX}".`);
+})().catch(e=>{console.error(e);process.exit(1);});
← a96bd4e7 sweep-to-100.js — drives search-by-color coverage to 100%: s
·
back to Designer Wallcoverings
·
brands-page: luxe wordmark SVGs for 6 new high-inventory bra 79ccd409 →