← 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
M public/index.htmlM server.jsM site.config.json
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 => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[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 →