[object Object]

← back to Dw Pairs Well

pairs/coordinate: accept ?palette=#hex,#hex,... for external sources (wallco.ai design pages)

d7926bea45bc0b2d7c992361ad7060f3b5b125b1 · 2026-05-13 12:04:30 -0700 · Steve Abrams

Files touched

Diff

commit d7926bea45bc0b2d7c992361ad7060f3b5b125b1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 13 12:04:30 2026 -0700

    pairs/coordinate: accept ?palette=#hex,#hex,... for external sources (wallco.ai design pages)
---
 deploy/shopify/design-coordinate.liquid | 33 ++++++++------
 server.js                               | 77 ++++++++++++++++++++++++++-------
 2 files changed, 81 insertions(+), 29 deletions(-)

diff --git a/deploy/shopify/design-coordinate.liquid b/deploy/shopify/design-coordinate.liquid
index ade08de..e6f1e6d 100644
--- a/deploy/shopify/design-coordinate.liquid
+++ b/deploy/shopify/design-coordinate.liquid
@@ -161,7 +161,13 @@
       var endpointAi    = root.getAttribute('data-ai-endpoint');
       var handle        = root.getAttribute('data-product-handle');
       var sku           = root.getAttribute('data-product-sku');
-      if (!(sku && sku.length) && !(handle && handle.length)) return;
+      // For external sources (wallco.ai design page, etc.) that aren't in our
+      // Shopify catalog: pass a comma-separated hex palette + title instead.
+      var palette       = root.getAttribute('data-source-palette');
+      var sourceTitle   = root.getAttribute('data-source-title') || '';
+      var sourceImage   = root.getAttribute('data-source-image') || '';
+      var hasIdentifier = (sku && sku.length) || (handle && handle.length) || (palette && palette.length);
+      if (!hasIdentifier) return;
       root.hidden = false;
 
       var cta            = root.querySelector('.pairs-well__cta');
@@ -296,8 +302,15 @@
 
       function buildUrl(base, limit) {
         var qs = [];
-        if (sku)    qs.push('dw_sku=' + encodeURIComponent(sku));
-        if (handle) qs.push('handle=' + encodeURIComponent(handle));
+        // External source (wallco design, etc.) takes precedence — passes palette directly
+        if (palette) {
+          qs.push('palette=' + encodeURIComponent(palette));
+          if (sourceTitle) qs.push('title=' + encodeURIComponent(sourceTitle));
+          if (sourceImage) qs.push('image_url=' + encodeURIComponent(sourceImage));
+        } else {
+          if (sku)    qs.push('dw_sku=' + encodeURIComponent(sku));
+          if (handle) qs.push('handle=' + encodeURIComponent(handle));
+        }
         if (limit)  qs.push('limit=' + limit);
         return base + '?' + qs.join('&');
       }
@@ -323,24 +336,16 @@
               root.hidden = true;
             }
           })
-          .catch(function (e) {
-            try { console.error('[design-coordinate] catalog load failed:', e, 'url=', buildUrl(endpointPairs, 24)); } catch (_) {}
-            var msg = (e && e.message) ? String(e.message).slice(0, 80) : 'unknown error';
-            catalogGrid.innerHTML = '<div class="pairs-well__empty">Could not load coordinates right now. <span style="opacity:.6;font-size:11px">('+msg+')</span></div>';
-          });
+          .catch(function () { catalogGrid.innerHTML = '<div class="pairs-well__empty">Could not load coordinates right now.</div>'; });
 
         var aiPromise = fetch(buildUrl(endpointAi, 24), { credentials: 'omit' })
           .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
           .then(function (d) {
-            if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+            if (!d || d.ok === false) throw new Error('bad payload');
             cachedAi = d.items || [];
             renderGrid(aiGrid, cachedAi, true);
           })
-          .catch(function (e) {
-            try { console.error('[design-coordinate] ai load failed:', e, 'url=', buildUrl(endpointAi, 24)); } catch (_) {}
-            var msg = (e && e.message) ? String(e.message).slice(0, 80) : 'unknown error';
-            aiGrid.innerHTML = '<div class="pairs-well__empty">AI generator not ready. <span style="opacity:.6;font-size:11px">('+msg+')</span></div>';
-          });
+          .catch(function () { aiGrid.innerHTML = '<div class="pairs-well__empty">AI generator not ready.</div>'; });
 
         return Promise.all([pairsPromise, aiPromise]);
       }
diff --git a/server.js b/server.js
index 6a1b91e..86a4f42 100644
--- a/server.js
+++ b/server.js
@@ -48,6 +48,19 @@ app.get('/healthz', async (_req, res) => {
 });
 
 const SAFE_KEY = /^[A-Za-z0-9._\-]{1,128}$/;
+const HEX_RE   = /^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
+
+// Parse a `palette=` query — comma-separated hex codes. Used by external sources
+// (e.g. wallco.ai design pages) that aren't in shopify_products but still want
+// to get coordinating stripes/plaids matched to a color story.
+function parsePaletteParam(raw) {
+  if (!raw) return [];
+  return String(raw).split(',')
+    .map(s => s.trim())
+    .filter(s => HEX_RE.test(s))
+    .map(s => s.startsWith('#') ? s : ('#' + s))
+    .slice(0, 8);
+}
 
 const SOURCE_COLS = `
   sp.dw_sku, sp.handle, sp.title, sp.vendor, sp.image_url, sp.tags, sp.pattern_name,
@@ -280,21 +293,45 @@ function pickPairs(source, candidates, k = 24) {
 
 app.get('/api/pairs', async (req, res) => {
   try {
-    const dw_sku = req.query.dw_sku && SAFE_KEY.test(req.query.dw_sku) ? req.query.dw_sku : null;
-    const handle = req.query.handle && SAFE_KEY.test(req.query.handle) ? req.query.handle : null;
-    if (!dw_sku && !handle) {
-      return res.status(400).json({ ok: false, error: 'pass ?dw_sku=… or ?handle=…' });
+    const dw_sku  = req.query.dw_sku && SAFE_KEY.test(req.query.dw_sku) ? req.query.dw_sku : null;
+    const handle  = req.query.handle && SAFE_KEY.test(req.query.handle) ? req.query.handle : null;
+    const paletteParam = parsePaletteParam(req.query.palette);
+    if (!dw_sku && !handle && paletteParam.length === 0) {
+      return res.status(400).json({ ok: false, error: 'pass ?dw_sku=…, ?handle=…, or ?palette=#hex1,#hex2,…' });
     }
 
-    const source = await loadSource({ dw_sku, handle });
-    if (!source) return res.status(404).json({ ok: false, error: 'source not found' });
+    // Synthetic source from palette param (used by wallco.ai design pages,
+    // any external integration where the source isn't in shopify_products).
+    let source;
+    if (paletteParam.length) {
+      source = {
+        dw_sku: null,
+        handle: null,
+        title: String(req.query.title || 'External source').slice(0, 200),
+        vendor: String(req.query.vendor || '').slice(0, 100),
+        image_url: String(req.query.image_url || '').slice(0, 1000) || null,
+        tags: null,
+        pattern_name: null,
+        dominant_hex: paletteParam[0],
+        background_hex: paletteParam[1] || null,
+        hex_codes: JSON.stringify(paletteParam),
+        color_family: null
+      };
+    } else {
+      source = await loadSource({ dw_sku, handle });
+      if (!source) return res.status(404).json({ ok: false, error: 'source not found' });
+    }
 
-    if (!source.tags || !parseTags(source.tags).length) {
+    // Bail only if we have NEITHER tags NOR palette — palette alone is enough
+    // for the palette-overlap path to find coordinates.
+    const sourceHasTags    = source.tags && parseTags(source.tags).length > 0;
+    const sourceHasPalette = paletteOf(source).length > 0;
+    if (!sourceHasTags && !sourceHasPalette) {
       return res.json({
         ok: true,
         source: { dw_sku: source.dw_sku, handle: source.handle, title: source.title, image_url: source.image_url, vendor: source.vendor },
         pairs: [],
-        reason: 'no tags on source — cannot compute pairings'
+        reason: 'no tags or palette on source — cannot compute pairings'
       });
     }
 
@@ -405,15 +442,25 @@ function svgPoster({ palette, motif, idx }) {
 
 app.get('/api/ai-coordinates', async (req, res) => {
   try {
-    const dw_sku = req.query.dw_sku && SAFE_KEY.test(req.query.dw_sku) ? req.query.dw_sku : null;
-    const handle = req.query.handle && SAFE_KEY.test(req.query.handle) ? req.query.handle : null;
-    if (!dw_sku && !handle) return res.status(400).json({ ok: false, error: 'pass dw_sku or handle' });
+    const dw_sku  = req.query.dw_sku && SAFE_KEY.test(req.query.dw_sku) ? req.query.dw_sku : null;
+    const handle  = req.query.handle && SAFE_KEY.test(req.query.handle) ? req.query.handle : null;
+    const paletteParam = parsePaletteParam(req.query.palette);
+    if (!dw_sku && !handle && paletteParam.length === 0) {
+      return res.status(400).json({ ok: false, error: 'pass dw_sku, handle, or palette' });
+    }
     const k = Math.max(1, Math.min(24, parseInt(req.query.limit, 10) || 24));
 
-    const source = await loadSource({ dw_sku, handle });
-    if (!source) return res.status(404).json({ ok: false, error: 'source not found' });
-
-    const palette = paletteOf(source);
+    let source, palette;
+    if (paletteParam.length) {
+      source = { dw_sku: null, handle: null,
+        title: String(req.query.title || 'External source').slice(0, 200),
+        image_url: String(req.query.image_url || '').slice(0, 1000) || null };
+      palette = paletteParam;
+    } else {
+      source = await loadSource({ dw_sku, handle });
+      if (!source) return res.status(404).json({ ok: false, error: 'source not found' });
+      palette = paletteOf(source);
+    }
     if (!palette.length) return res.json({ ok: true, source: { dw_sku: source.dw_sku, handle: source.handle }, items: [] });
 
     // 16 stripes + 8 plaids = 24 by default; ordering shuffles so the strongest

← c54aef9 preview.html: accept handle (auto-detect vs dw_sku), deep-li  ·  back to Dw Pairs Well  ·  preview.html: gate default DWRW-74991 init on window.__deepL fb9ee05 →