← 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
M deploy/shopify/design-coordinate.liquidM server.js
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 →