[object Object]

← back to Dw Yolo Loop

Artmura site (B): 'Pairs well with' recs + per-product JSON-LD on PDP

528cdf42a25f07b7987018b1cc3c5fc3495233b0 · 2026-06-12 09:27:11 -0700 · Steve Abrams

- /api/pairs/:handle — interior-designer logic (same/adjacent color family, different motif series, same book bonus); 6 picks
- PDP pairs row (6 swatches) + schema.org Product JSON-LD (sku, brand, image, offer)

Files touched

Diff

commit 528cdf42a25f07b7987018b1cc3c5fc3495233b0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 12 09:27:11 2026 -0700

    Artmura site (B): 'Pairs well with' recs + per-product JSON-LD on PDP
    
    - /api/pairs/:handle — interior-designer logic (same/adjacent color family, different motif series, same book bonus); 6 picks
    - PDP pairs row (6 swatches) + schema.org Product JSON-LD (sku, brand, image, offer)
---
 artmura-site/public/product.html | 26 +++++++++++++++++++++++++-
 artmura-site/public/styles.css   | 11 +++++++++++
 artmura-site/server.js           | 18 ++++++++++++++++++
 3 files changed, 54 insertions(+), 1 deletion(-)

diff --git a/artmura-site/public/product.html b/artmura-site/public/product.html
index dc2ba3e..16a972f 100644
--- a/artmura-site/public/product.html
+++ b/artmura-site/public/product.html
@@ -65,7 +65,31 @@ function specRow(k,v){return v?`<div class="row"><span class="k">${k}</span><spa
       <div><h3>The story</h3><div>${p.body_html||'<p>'+esc(p.title)+' by Artmura.</p>'}</div></div>
       <div><h3>Specifications</h3><p>${esc(p.width||'')} · ${esc(p.substrate||'Non-Woven')} · ${esc(p.grade||'Residential')} grade · ${esc(p.coverage||'')} · ${esc(p.origin||'Made in Italy')}.</p></div>
       <div><h3>Care &amp; install</h3><p>Paste-the-wall non-woven · spongeable · lead-time ${esc(p.lead_time||'3–4 weeks')}. Order a sample before specifying.</p></div>
-    </div>`;
+    </div>
+    <section class="pairs" id="pairs" hidden>
+      <h3>Pairs well with</h3>
+      <div class="pair-grid" id="pairGrid"></div>
+    </section>`;
+
+  // JSON-LD Product schema
+  const ld={ "@context":"https://schema.org","@type":"Product",name:p.title+" by Artmura",sku:p.sku,
+    brand:{"@type":"Brand",name:"Artmura"},category:"Wallcoverings",image:imgs,
+    description:(p.body_html||'').replace(/<[^>]+>/g,'').slice(0,300),
+    offers:{"@type":"Offer",priceCurrency:"USD",price:String(p.price||''),availability:"https://schema.org/InStock"}};
+  const s=document.createElement('script');s.type='application/ld+json';s.textContent=JSON.stringify(ld);document.head.appendChild(s);
+
+  // pairs well with
+  try{
+    const pr=await (await fetch('/api/pairs/'+handle)).json();
+    if(pr.pairs&&pr.pairs.length){
+      document.getElementById('pairGrid').innerHTML=pr.pairs.map(q=>`
+        <a class="pcard" href="/product/${q.handle}">
+          <img loading="lazy" decoding="async" alt="${esc(q.title)}" src="${q.swatch||q.room||''}">
+          <div class="pc-b"><span class="pc-s">${esc(q.series||'')}</span><span class="pc-n">${esc(q.color||q.title)}</span></div>
+        </a>`).join('');
+      document.getElementById('pairs').hidden=false;
+    }
+  }catch(e){}
 
   // gallery swap
   document.querySelectorAll('.thumbs .t').forEach(t=>t.onclick=()=>{
diff --git a/artmura-site/public/styles.css b/artmura-site/public/styles.css
index f27a152..7083110 100644
--- a/artmura-site/public/styles.css
+++ b/artmura-site/public/styles.css
@@ -153,6 +153,17 @@ footer{border-top:1px solid var(--line);background:var(--paper)}
   padding-top:40px;border-top:1px solid var(--line)}
 .story h3{font-family:var(--serif);font-weight:500;font-size:22px;margin:0 0 12px}
 .story p{font-size:13.5px;color:var(--ink-soft);margin:0}
+.pairs{margin-top:60px;padding-top:40px;border-top:1px solid var(--line)}
+.pairs h3{font-family:var(--serif);font-weight:500;font-size:24px;margin:0 0 22px}
+.pair-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:18px}
+.pcard{display:block}
+.pcard img{aspect-ratio:1/1;width:100%;object-fit:cover;border:1px solid var(--line);border-radius:3px;transition:.25s}
+.pcard:hover img{transform:translateY(-3px);box-shadow:0 14px 32px -20px rgba(33,29,24,.5)}
+.pc-b{padding:9px 2px}
+.pc-s{display:block;font-size:9px;letter-spacing:.16em;text-transform:uppercase;color:var(--gold)}
+.pc-n{display:block;font-family:var(--serif);font-size:16px;line-height:1.1;margin-top:2px}
+@media(max-width:1080px){.pair-grid{grid-template-columns:repeat(3,1fr)}}
+@media(max-width:560px){.pair-grid{grid-template-columns:repeat(2,1fr)}}
 .preview-flag{position:fixed;bottom:0;left:0;right:0;z-index:50;background:#211d18;color:#f6f2ec;
   font-size:11px;letter-spacing:.05em;text-align:center;padding:7px 14px}
 .preview-flag b{color:#caa45f}
diff --git a/artmura-site/server.js b/artmura-site/server.js
index 9b34a70..64b349a 100644
--- a/artmura-site/server.js
+++ b/artmura-site/server.js
@@ -67,6 +67,24 @@ app.get('/api/product/:handle', (req, res) => {
   if (!p) return res.status(404).json({ error: 'not found' });
   res.json(p);
 });
+// "Pairs well with" — interior-designer logic: same/adjacent color family, different
+// pattern series (contrast of scale), prefer same collection book. Returns up to 6.
+app.get('/api/pairs/:handle', (req, res) => {
+  const p = CATALOG.find(x => x.handle === req.params.handle);
+  if (!p) return res.status(404).json({ error: 'not found' });
+  const hueDist = (a, b) => { if (a == null || b == null) return 180; const d = Math.abs(a - b) % 360; return d > 180 ? 360 - d : d; };
+  const scored = CATALOG.filter(x => x.handle !== p.handle).map(x => {
+    let s = 0;
+    if (x.color_bucket && x.color_bucket === p.color_bucket) s += 40;       // same color family
+    s += Math.max(0, 30 - hueDist(x.hue, p.hue) / 2);                        // hue proximity
+    if (x.series !== p.series) s += 20;                                      // different motif/scale
+    if (x.book && x.book === p.book) s += 12;                                // same book
+    if (x.color_bucket && ['grey', 'white', 'black'].includes(x.color_bucket) && !['grey','white','black'].includes(p.color_bucket)) s += 8; // neutral pairs with chromatic
+    return { x, s };
+  }).sort((a, b) => b.s - a.s).slice(0, 6).map(o => o.x);
+  res.json({ pairs: scored });
+});
+
 app.get('/api/facets', (_req, res) => {
   const books = {}, series = {}, colors = {};
   for (const p of CATALOG) {

← 3e3a3bd Artmura site (B/catalog): dominant-color extraction → true-h  ·  back to Dw Yolo Loop  ·  Artmura site: README documenting features, data, endpoints, 7a52550 →