← back to Customdigitalmurals
homepage rails: 6 aesthetic rail sections + pills strip
0aa321f9ca15bd7005e0fe99ca7c60795d53e848 · 2026-05-26 08:14:02 -0700 · Steve
Adds /api/sliders endpoint, aesthetic filter on /api/products, aesthetic
counts on /api/facets. homePage() now renders 4 rail sections (green/
botanical/commercial/blue, 8 cards each, View all →) above the shop +
horizontal pill strip wired to setAesthetic(). Click any pill or 'View all'
re-renders the grid filtered by that aesthetic and scrolls to #shop.
Pattern from hollywood-wallcoverings 3a7da85 + blockprintedwallpaper.
Bucket distribution: green 1638 / botanical 1537 / commercial 422 / blue
24 / white 20 / architectural 14 (all 6 rails fire, all >= 4 items).
Files touched
Diff
commit 0aa321f9ca15bd7005e0fe99ca7c60795d53e848
Author: Steve <steve@designerwallcoverings.com>
Date: Tue May 26 08:14:02 2026 -0700
homepage rails: 6 aesthetic rail sections + pills strip
Adds /api/sliders endpoint, aesthetic filter on /api/products, aesthetic
counts on /api/facets. homePage() now renders 4 rail sections (green/
botanical/commercial/blue, 8 cards each, View all →) above the shop +
horizontal pill strip wired to setAesthetic(). Click any pill or 'View all'
re-renders the grid filtered by that aesthetic and scrolls to #shop.
Pattern from hollywood-wallcoverings 3a7da85 + blockprintedwallpaper.
Bucket distribution: green 1638 / botanical 1537 / commercial 422 / blue
24 / white 20 / architectural 14 (all 6 rails fire, all >= 4 items).
---
server.js | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 94 insertions(+), 3 deletions(-)
diff --git a/server.js b/server.js
index ad682d5..04fba2c 100644
--- a/server.js
+++ b/server.js
@@ -73,6 +73,7 @@ app.get('/health', (_req, res) => res.json({ ok: true, count: ALL.length }));
app.get('/api/products', (req, res) => {
const q = (req.query.q || '').toLowerCase().trim();
const vendor = (req.query.vendor || '').toLowerCase().trim();
+ const aesthetic = (req.query.aesthetic || '').toLowerCase().trim();
const sort = req.query.sort || 'az';
const page = Math.max(1, parseInt(req.query.page) || 1);
const per = Math.min(120, Math.max(12, parseInt(req.query.per) || 60));
@@ -90,6 +91,9 @@ app.get('/api/products', (req, res) => {
if (vendor) {
results = results.filter(p => (p.vendor || '').toLowerCase() === vendor);
}
+ if (aesthetic && aesthetic !== 'all') {
+ results = results.filter(p => String(p.aesthetic || '').toLowerCase() === aesthetic);
+ }
// Sort
if (sort === 'az') results = [...results].sort((a, b) => a.title.localeCompare(b.title));
@@ -104,6 +108,16 @@ app.get('/api/products', (req, res) => {
res.json({ total, pages, page, per, products: slice });
});
+// Aesthetic rails — used by homepage rail-sections render.
+app.get('/api/sliders', (_req, res) => {
+ const out = [];
+ for (const a of SITE_RAILS) {
+ const items = ALL.filter(p => String(p.aesthetic || '').toLowerCase() === a.toLowerCase()).slice(0, 12);
+ if (items.length >= 4) out.push({ aesthetic: a, items });
+ }
+ res.json({ rails: out });
+});
+
// Vendor list (LEGACY — kept for back-compat; new code should use /api/facets).
// Vendor names are admin-only metadata and must NOT render on customer-facing UI.
app.get('/api/vendors', (_req, res) => {
@@ -138,9 +152,11 @@ app.get('/api/facets', (req, res) => {
const vendors = {};
const productTypes = {};
+ const aesthetics = {};
filtered.forEach(p => {
if (p.vendor) vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
if (p.product_type) productTypes[p.product_type] = (productTypes[p.product_type] || 0) + 1;
+ if (p.aesthetic) aesthetics[String(p.aesthetic).toLowerCase()] = (aesthetics[String(p.aesthetic).toLowerCase()] || 0) + 1;
});
const toList = obj => Object.entries(obj)
@@ -151,6 +167,7 @@ app.get('/api/facets', (req, res) => {
total: filtered.length,
vendors: toList(vendors),
product_types: toList(productTypes),
+ aesthetics,
});
});
@@ -427,7 +444,29 @@ ${headerHtml('home')}
</div>
</section>
+<!-- Aesthetic rails (added 2026-05-26 fleet rails sweep) -->
+<style>
+ .cdm-rails { max-width: 1400px; margin: 0 auto; padding: 48px 32px 0; }
+ .rail-section { margin-bottom: 56px; }
+ .rail-head { display:flex; align-items:baseline; justify-content:space-between; margin-bottom:18px; padding-bottom:10px; border-bottom:1px solid var(--line); }
+ .rail-title-h { font-size: 22px; letter-spacing: .18em; text-transform: uppercase; font-weight: 700; margin: 0; color: var(--ink); }
+ .rail-all { font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--ink-soft); font-weight: 600; cursor: pointer; background: transparent; border: 0; padding: 0; font-family: inherit; }
+ .rail-all:hover { color: var(--ink); }
+ .rail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 18px; }
+ .rail-card { display: block; aspect-ratio: 1/1; overflow: hidden; position: relative; cursor: pointer; }
+ .rail-card img { width: 100%; height: 100%; object-fit: cover; object-position: center 12%; transition: transform .4s ease; }
+ .rail-card:hover img { transform: scale(1.04); }
+ .rail-card-label { position: absolute; bottom: 0; left: 0; right: 0; padding: 12px 14px; background: linear-gradient(to top, rgba(0,0,0,0.7), transparent); color: #fff; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
+ .cdm-pills { display: flex; gap: 10px; flex-wrap: wrap; padding: 18px 32px 0; max-width: 1400px; margin: 0 auto; }
+ .cdm-pill { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid var(--line); background: transparent; color: var(--ink-soft); font: inherit; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; font-weight: 600; cursor: pointer; border-radius: 0; transition: border-color .15s, color .15s, background .15s; }
+ .cdm-pill:hover { border-color: var(--ink); color: var(--ink); }
+ .cdm-pill.active { border-color: var(--ink); color: var(--bg); background: var(--ink); }
+ .cdm-pill span { font-size: 10px; opacity: .7; font-weight: 700; }
+</style>
+<section class="cdm-rails" id="railSections" aria-label="Browse by aesthetic"></section>
+
<div id="shop">
+<nav class="cdm-pills" id="cdmPills" aria-label="Filter by aesthetic"></nav>
<div class="toolbar">
<input type="text" id="search" placeholder="Search by title, keyword..." autocomplete="off">
<select id="sort">
@@ -456,8 +495,9 @@ ${footerHtml()}
<script>
var page=1, per=60, total=0, pages=0;
-var q='', sort='az';
-var LS_SORT='cdm-sort', LS_DENSITY='cdm-density';
+var q='', sort='az', aesthetic='all';
+var LS_SORT='cdm-sort', LS_DENSITY='cdm-density', LS_AESTHETIC='cdm-aesthetic';
+try { var savedA = localStorage.getItem(LS_AESTHETIC); if (savedA) aesthetic = savedA; } catch(e){}
// Hydrate saved sort + density BEFORE the first grid load.
try {
@@ -483,7 +523,7 @@ document.getElementById('density').addEventListener('input',function(){
setCols(savedCols);
function load(){
- var url='/api/products?page='+page+'&per='+per+'&q='+encodeURIComponent(q)+'&sort='+sort;
+ var url='/api/products?page='+page+'&per='+per+'&q='+encodeURIComponent(q)+'&sort='+sort+'&aesthetic='+encodeURIComponent(aesthetic);
fetch(url).then(r=>r.json()).then(data=>{
total=data.total; pages=data.pages;
document.getElementById('count').textContent=total.toLocaleString()+' murals';
@@ -526,6 +566,57 @@ document.getElementById('sort').addEventListener('change',function(){
load();
});
+// ── Aesthetic rails + pills (fleet rails sweep 2026-05-26) ───────────────
+function setAesthetic(a){
+ aesthetic = a || 'all';
+ try { localStorage.setItem(LS_AESTHETIC, aesthetic); } catch(e){}
+ page = 1;
+ document.querySelectorAll('#cdmPills .cdm-pill').forEach(function(p){
+ p.classList.toggle('active', p.dataset.aesthetic === aesthetic);
+ });
+ load();
+ var shop = document.getElementById('shop');
+ if (shop) shop.scrollIntoView({ behavior: 'smooth', block: 'start' });
+}
+function renderPills(facets){
+ var bar = document.getElementById('cdmPills');
+ if (!bar) return;
+ var aesth = (facets && facets.aesthetics) || {};
+ var total = (facets && facets.total) || 0;
+ var entries = Object.entries(aesth).filter(function(e){ return e[0] && e[1]>=4; }).sort(function(a,b){ return b[1]-a[1]; }).slice(0,8);
+ var html = '<button type="button" class="cdm-pill'+(aesthetic==='all'?' active':'')+'" data-aesthetic="all">All <span>'+total.toLocaleString()+'</span></button>';
+ entries.forEach(function(e){
+ html += '<button type="button" class="cdm-pill'+(aesthetic===e[0]?' active':'')+'" data-aesthetic="'+esc(e[0])+'">'+esc(e[0].charAt(0).toUpperCase()+e[0].slice(1))+' <span>'+e[1].toLocaleString()+'</span></button>';
+ });
+ bar.innerHTML = html;
+ bar.querySelectorAll('.cdm-pill').forEach(function(p){
+ p.addEventListener('click', function(){ setAesthetic(p.dataset.aesthetic); });
+ });
+}
+function renderRailSections(rails){
+ var host = document.getElementById('railSections');
+ if (!host || !rails || !rails.length) return;
+ host.innerHTML = rails.slice(0,4).map(function(r){
+ var cards = (r.items||[]).slice(0,8).map(function(p){
+ var img = p.images && p.images[0] ? p.images[0] : '';
+ var href = '/product/'+encodeURIComponent(p.handle||'');
+ return '<a class="rail-card" href="'+esc(href)+'">'+(img?'<img src="'+esc(img)+'" alt="'+esc(p.title||'')+'" loading="lazy">':'')+'<span class="rail-card-label">'+esc(p.title||'')+'</span></a>';
+ }).join('');
+ var label = r.aesthetic.charAt(0).toUpperCase()+r.aesthetic.slice(1);
+ return '<section class="rail-section"><div class="rail-head"><h2 class="rail-title-h">'+esc(label)+'</h2><button type="button" class="rail-all" data-aesthetic="'+esc(r.aesthetic)+'">View all →</button></div><div class="rail-grid">'+cards+'</div></section>';
+ }).join('');
+ host.querySelectorAll('.rail-all').forEach(function(b){
+ b.addEventListener('click', function(){ setAesthetic(b.dataset.aesthetic); });
+ });
+}
+Promise.all([
+ fetch('/api/facets').then(function(r){return r.json();}).catch(function(){return null;}),
+ fetch('/api/sliders').then(function(r){return r.json();}).catch(function(){return null;})
+]).then(function(rs){
+ if (rs[0]) renderPills(rs[0]);
+ if (rs[1] && rs[1].rails) renderRailSections(rs[1].rails);
+});
+
load();
</script>
</body></html>`;
← 55825b0 wire api-vendor-redact middleware (close vendors-facet leak)
·
back to Customdigitalmurals
·
scrub vendor name leaks from products.json source (3132 prod c8239a8 →