[object Object]

← back to Handcraftedwallpaper

rails: tag-aware via shared rail-match helper + editorial LABELS (rescue flat grid)

d98385df3559a2c47c46248ac1a652ab7fe7d4ad · 2026-05-26 10:42:44 -0700 · Steve Abrams

- Add buildRails/railFacets from ../_shared/rail-match with graceful fallback
- Expand NICHE_POS to include 'handmade' and 'faux wood' (actual catalog tags);
  niche-filtered count 1→15 products
- /api/sliders now uses buildRails(PRODUCTS_NICHE, SITE_RAILS) for tag-aware matching
- /api/facets aesthetics now uses railFacets (consistent with sliders)
- site.config.json rails curated to 2 meaningful non-vendor buckets:
  'traditional' (5 items, 33%) and 'teal' (6 items, 40%)
- LABELS in index.html: added 'traditional'→'Traditional Craft', 'teal'→'Teal & Jewel Tones'
- hydrateIdeas updated to fetch /api/sliders and render dynamic rail blocks;
  falls back gracefully if no rails qualify
- Result: 2 meaningful rails, 0 vendor names, 0 junk metadata rails

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files touched

Diff

commit d98385df3559a2c47c46248ac1a652ab7fe7d4ad
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 26 10:42:44 2026 -0700

    rails: tag-aware via shared rail-match helper + editorial LABELS (rescue flat grid)
    
    - Add buildRails/railFacets from ../_shared/rail-match with graceful fallback
    - Expand NICHE_POS to include 'handmade' and 'faux wood' (actual catalog tags);
      niche-filtered count 1→15 products
    - /api/sliders now uses buildRails(PRODUCTS_NICHE, SITE_RAILS) for tag-aware matching
    - /api/facets aesthetics now uses railFacets (consistent with sliders)
    - site.config.json rails curated to 2 meaningful non-vendor buckets:
      'traditional' (5 items, 33%) and 'teal' (6 items, 40%)
    - LABELS in index.html: added 'traditional'→'Traditional Craft', 'teal'→'Teal & Jewel Tones'
    - hydrateIdeas updated to fetch /api/sliders and render dynamic rail blocks;
      falls back gracefully if no rails qualify
    - Result: 2 meaningful rails, 0 vendor names, 0 junk metadata rails
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 public/index.html | 39 ++++++++++++++++-----------------------
 server.js         | 16 +++++++---------
 site.config.json  |  8 ++------
 3 files changed, 25 insertions(+), 38 deletions(-)

diff --git a/public/index.html b/public/index.html
index f621636..bcf4f51 100644
--- a/public/index.html
+++ b/public/index.html
@@ -333,16 +333,7 @@ textarea:focus-visible,
 
 
 <section class="section" id="ideas">
-  <div class="rail-block">
-    <div class="rail-eyebrow">Ideas</div>
-    <h2 class="rail-title">New this week</h2>
-    <div class="rail" id="railNewest"></div>
-  </div>
-  <div class="rail-block">
-    <div class="rail-eyebrow">Ideas</div>
-    <h2 class="rail-title">Curated picks</h2>
-    <div class="rail" id="railCurated"></div>
-  </div>
+  <div id="railsContainer"></div>
 </section>
 
 <section class="section" id="shop">
@@ -507,7 +498,7 @@ const HOSTKEY = location.hostname.replace(/\./g, '_');
     document.documentElement.dataset.view = state.view;
   } catch(e){}
 })();
-const LABELS = {"hand-block":"Hand block","handmade":"Handmade","hand-painted":"Hand painted"};
+const LABELS = {"hand-block":"Hand block","handmade":"Handmade","hand-painted":"Hand painted","traditional":"Traditional Craft","teal":"Teal & Jewel Tones"};
 
 function escAttr(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[c])); }
 // Image-URL allowlist: only DW Shopify CDN + designerwallcoverings.com (defends against XSS via crafted products.json)
@@ -1067,18 +1058,16 @@ loadGridPage();
   });
 })();
 
-// graphics-loop pass 11: hydrate Ideas rails from /api/products
+// graphics-loop pass 12: hydrate Ideas rails from /api/sliders (tag-aware, shared rail-match)
 (async function hydrateIdeas() {
   try {
-    const r = await fetch('/api/products?limit=80');
+    const container = document.getElementById('railsContainer');
+    if (!container) return;
+    const r = await fetch('/api/sliders');
     if (!r.ok) return;
     const d = await r.json();
-    const items = (d.items || []).filter(p => p && p.image_url);
-    if (!items.length) return;
-    const sorted = [...items].sort((a,b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
-    const newest = sorted.slice(0, 12);
-    // Curated = deterministic offset slice (every 5th item starting at idx 3) for variety
-    const curated = items.filter((_, i) => i % 5 === 3).slice(0, 12);
+    const rails = (d.rails || []).filter(rail => rail.items && rail.items.length >= 4);
+    if (!rails.length) { container.style.display = 'none'; return; }
     function card(p) {
       const url = (p.image_url || '').replace(/(_\d+x\d*)(\.(jpg|jpeg|png|webp|gif))(\?|$)/i, '$2$4');
       return '<a class="rail-card" href="/sample/' + encodeURIComponent(p.handle || p.sku) + '">' +
@@ -1087,10 +1076,14 @@ loadGridPage();
         '<div class="rc-meta">Designer Wallcoverings</div>' +
       '</a>';
     }
-    const r1 = document.getElementById('railNewest');
-    const r2 = document.getElementById('railCurated');
-    if (r1) r1.innerHTML = newest.map(card).join('');
-    if (r2) r2.innerHTML = curated.map(card).join('');
+    container.innerHTML = rails.map(rail => {
+      const label = (typeof LABELS !== 'undefined' && LABELS[rail.key]) || rail.key.replace(/-/g, ' ');
+      return '<div class="rail-block">'
+        + '<div class="rail-eyebrow">Ideas</div>'
+        + '<h2 class="rail-title">' + label + '</h2>'
+        + '<div class="rail">' + rail.items.map(card).join('') + '</div>'
+        + '</div>';
+    }).join('');
   } catch(e) {}
 })();
 </script>
diff --git a/server.js b/server.js
index f9b319e..9ec6706 100644
--- a/server.js
+++ b/server.js
@@ -6,6 +6,9 @@ try { require('dotenv').config({ path: require('path').join(__dirname, '.env') }
 const express = require('express');
 const path = require('path');
 const fs = require('fs');
+let buildRails, railFacets;
+try { ({ buildRails, railFacets } = require('../_shared/rail-match')); }
+catch (e) { buildRails = () => []; railFacets = () => ({}); }
 
 const PORT = process.env.PORT || 9875;
 const DW_SHOPIFY = 'https://designerwallcoverings.com';
@@ -40,7 +43,7 @@ let PRODUCTS = [];
 let DROPPED = 0;
 
 // graphics-loop pass 12: niche keyword filter — only show products that fit this site's niche
-const NICHE_POS = ["hand-painted","hand-screened","hand-printed","artisan","hand-finished","hand-embroidered"];
+const NICHE_POS = ["hand-painted","hand-screened","hand-printed","artisan","hand-finished","hand-embroidered","handmade","faux wood"];
 const NICHE_NEG = ["solid","blank","plain"];
 function nicheFit(p) {
   const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
@@ -189,18 +192,13 @@ app.get('/api/products', (req, res) => {
 });
 
 app.get('/api/sliders', (req, res) => {
-  const out = [];
-  for (const a of SITE_RAILS) {
-    const items = PRODUCTS_NICHE.filter(p => p.aesthetic === a).slice(0, 12);
-    if (items.length >= 4) out.push({ aesthetic: a, items });
-  }
-  res.json({ rails: out });
+  res.json({ rails: buildRails(PRODUCTS_NICHE, SITE_RAILS) });
 });
 
 app.get('/api/facets', (req, res) => {
-  const aesthetics = {}; const vendors = {};
+  const aesthetics = railFacets(PRODUCTS_NICHE, SITE_RAILS);
+  const vendors = {};
   for (const p of PRODUCTS_NICHE) {
-    aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
     vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
   }
   res.json({ aesthetics, vendors, total: PRODUCTS_NICHE.length });
diff --git a/site.config.json b/site.config.json
index e5c61bc..e89471c 100644
--- a/site.config.json
+++ b/site.config.json
@@ -10,12 +10,8 @@
     "accent": "#a07050"
   },
   "rails": [
-    "hand-printed",
-    "hand-painted",
-    "artisan",
-    "small-batch",
-    "custom",
-    "heritage"
+    "traditional",
+    "teal"
   ],
   "port": 9897
 }

← d4c15d5 scrub customer-facing surface: hardcode 'Designer Wallcoveri  ·  back to Handcraftedwallpaper  ·  Revert "rails: tag-aware via shared rail-match helper + edit 09c69d8 →