← back to Wallsandfabrics
snapshot before hamburger nav rollout
7aff7d823ed04954f57243edde188b4083ed5bfa · 2026-05-10 21:40:17 -0700 · Steve Abrams
Files touched
A public/index.html.pre-sort-frontendA server.js.pre-sort-skill
Diff
commit 7aff7d823ed04954f57243edde188b4083ed5bfa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun May 10 21:40:17 2026 -0700
snapshot before hamburger nav rollout
---
public/index.html.pre-sort-frontend | 267 ++++++++++++++++++++++++++++++++++++
server.js.pre-sort-skill | 159 +++++++++++++++++++++
2 files changed, 426 insertions(+)
diff --git a/public/index.html.pre-sort-frontend b/public/index.html.pre-sort-frontend
new file mode 100644
index 0000000..c7b8848
--- /dev/null
+++ b/public/index.html.pre-sort-frontend
@@ -0,0 +1,267 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Walls & Fabrics — wallcoverings + fabrics from Designer Wallcoverings</title>
+<meta name="description" content="One unified search across wallcoverings, fabrics, murals and trims. Memo samples shipped fast. Trade-friendly pricing from Designer Wallcoverings.">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+
+<!-- anti-flash theme — must run before paint -->
+<script>
+(function(){try{var t=localStorage.getItem('waf-theme');if(!t){t=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}document.documentElement.setAttribute('data-theme',t);}catch(e){}})();
+</script>
+
+<!-- GA4 gtag (auto) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-429EVNMHVY"></script>
+<script>
+window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}
+gtag('js', new Date());
+gtag('config','G-429EVNMHVY');
+</script>
+<!-- /GA4 gtag -->
+
+<style>
+:root{
+ --bg:#F4EFE6; --fg:#0E1116; --muted:#5b5042; --line:#d8cfbf;
+ --paper:#ffffff; --accent:#9A6E3A; --accent2:#46606D;
+ --shadow:0 1px 0 rgba(0,0,0,.05),0 12px 28px -16px rgba(0,0,0,.18);
+ --cols:5;
+}
+:root[data-theme="dark"]{
+ --bg:#0E1116; --fg:#F0EBE0; --muted:#9aa1ae; --line:#2a3038;
+ --paper:#171b22; --accent:#C99463; --accent2:#7BA3B3;
+ --shadow:0 1px 0 rgba(0,0,0,.4),0 14px 30px -14px rgba(0,0,0,.6);
+}
+*{box-sizing:border-box}
+html,body{margin:0;background:var(--bg);color:var(--fg);font-family:Georgia,'Times New Roman',serif}
+body{font-size:16px;line-height:1.5}
+a{color:var(--accent);text-decoration:none}
+a:hover{text-decoration:underline}
+.wrap{max-width:1320px;margin:0 auto;padding:0 22px}
+header.site{position:sticky;top:0;z-index:20;background:var(--bg);border-bottom:1px solid var(--line)}
+header.site .row{display:flex;align-items:center;gap:18px;padding:14px 0}
+.brand{font-family:'Bodoni Moda','Playfair Display',Georgia,serif;font-weight:700;font-size:22px;letter-spacing:.02em}
+.brand .amp{color:var(--accent);font-style:italic;padding:0 .12em}
+nav.top{display:flex;gap:18px;margin-left:auto;align-items:center;font-size:14px}
+nav.top a{color:var(--fg);opacity:.78}
+nav.top a:hover{opacity:1}
+.theme-btn{background:none;border:1px solid var(--line);color:var(--fg);width:38px;height:38px;border-radius:50%;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:16px}
+.theme-btn:hover{border-color:var(--accent)}
+
+.hero{padding:64px 0 28px;border-bottom:1px solid var(--line)}
+.hero h1{font-family:'Bodoni Moda','Playfair Display',Georgia,serif;font-size:clamp(40px,6vw,72px);line-height:1;margin:0 0 16px;letter-spacing:-.01em}
+.hero h1 em{font-style:italic;color:var(--accent)}
+.hero p.lede{font-size:18px;color:var(--muted);max-width:62ch;margin:0 0 20px}
+.hero .meta{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.16em;margin-bottom:14px}
+.surface-tabs{display:flex;flex-wrap:wrap;gap:8px;margin:24px 0 0}
+.surface-tab{padding:10px 16px;border:1px solid var(--line);background:var(--paper);color:var(--fg);border-radius:999px;cursor:pointer;font:inherit;font-size:13px;letter-spacing:.05em;text-transform:uppercase}
+.surface-tab[aria-pressed="true"]{background:var(--fg);color:var(--bg);border-color:var(--fg)}
+.surface-tab .count{opacity:.6;margin-left:6px;font-variant-numeric:tabular-nums}
+
+.toolbar{display:flex;flex-wrap:wrap;gap:14px;align-items:center;padding:18px 0;border-bottom:1px solid var(--line);margin-bottom:24px}
+.toolbar .grow{flex:1;min-width:200px}
+.toolbar input[type="search"]{width:100%;padding:11px 14px;font:inherit;border:1px solid var(--line);background:var(--paper);color:var(--fg);border-radius:8px}
+.toolbar label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.12em}
+.toolbar select{padding:10px 12px;font:inherit;border:1px solid var(--line);background:var(--paper);color:var(--fg);border-radius:8px}
+.toolbar input[type="range"]{accent-color:var(--accent);width:140px}
+.toolbar .group{display:flex;align-items:center;gap:8px}
+
+.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:18px;padding:8px 0 60px}
+@media (max-width:1100px){.grid{grid-template-columns:repeat(min(var(--cols),4),1fr)}}
+@media (max-width:820px){.grid{grid-template-columns:repeat(min(var(--cols),3),1fr)}}
+@media (max-width:560px){.grid{grid-template-columns:repeat(min(var(--cols),2),1fr)}}
+.card{background:var(--paper);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;transition:transform .15s,box-shadow .15s}
+.card:hover{transform:translateY(-2px);box-shadow:var(--shadow)}
+.card .img{aspect-ratio:1/1;background:#0001;display:block;background-size:cover;background-position:center}
+.card .body{padding:11px 12px 12px}
+.card .surface{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);font-weight:700}
+.card .ttl{font-size:14px;line-height:1.35;margin:4px 0 6px;color:var(--fg);min-height:2.6em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
+.card .meta{font-size:12px;color:var(--muted);display:flex;justify-content:space-between;gap:8px}
+.card a.smp{display:inline-block;margin-top:8px;font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--accent2);border-bottom:1px solid var(--accent2);padding-bottom:1px}
+
+.pager{display:flex;justify-content:center;gap:10px;margin:30px 0 60px}
+.pager button{padding:10px 18px;border:1px solid var(--line);background:var(--paper);color:var(--fg);border-radius:6px;cursor:pointer;font:inherit;font-size:14px}
+.pager button:disabled{opacity:.4;cursor:not-allowed}
+.pager .info{align-self:center;color:var(--muted);font-size:13px;font-variant-numeric:tabular-nums}
+
+.empty{padding:80px 20px;text-align:center;color:var(--muted)}
+.empty h2{color:var(--fg);font-family:'Bodoni Moda',Georgia,serif;font-weight:400;margin-bottom:8px}
+
+footer.site{border-top:1px solid var(--line);padding:36px 0;font-size:13px;color:var(--muted);background:var(--paper)}
+footer.site .grid-foot{display:grid;grid-template-columns:2fr 1fr 1fr;gap:30px}
+@media (max-width:720px){footer.site .grid-foot{grid-template-columns:1fr}}
+footer.site h4{font-size:11px;text-transform:uppercase;letter-spacing:.18em;color:var(--fg);margin:0 0 10px}
+footer.site a{color:var(--accent2)}
+.legal{margin-top:18px;font-size:12px;color:var(--muted)}
+</style>
+</head>
+<body>
+
+<header class="site">
+ <div class="wrap row">
+ <div class="brand">Walls <span class="amp">&</span> Fabrics</div>
+ <nav class="top">
+ <a href="#grid" data-surface="all">All</a>
+ <a href="#grid" data-surface="wallcovering">Walls</a>
+ <a href="#grid" data-surface="fabric">Fabrics</a>
+ <a href="#grid" data-surface="mural">Murals</a>
+ <a href="#grid" data-surface="trim">Trim</a>
+ <button id="themeBtn" class="theme-btn" type="button" aria-label="Toggle dark / light">☼</button>
+ </nav>
+ </div>
+</header>
+
+<section class="hero wrap" id="hero">
+ <div class="meta">A Designer Wallcoverings property</div>
+ <h1>Walls <em>&</em> Fabrics.</h1>
+ <p class="lede">One unified search across the Designer Wallcoverings catalog of wallcoverings, fabrics, murals, and trims. Sort by color, style, or surface — request memo samples in a few clicks.</p>
+
+ <div class="surface-tabs" id="surfaceTabs"></div>
+</section>
+
+<section class="wrap" id="grid">
+ <div class="toolbar">
+ <div class="grow"><input id="q" type="search" placeholder="Search by name, color, vendor, surface…" autocomplete="off"></div>
+ <div class="group"><label for="sort">Sort</label>
+ <select id="sort">
+ <option value="newest">Newest</option>
+ <option value="color">Color</option>
+ <option value="style">Style</option>
+ <option value="sku">SKU A→Z</option>
+ <option value="title">Title A→Z</option>
+ <option value="price-asc">Price ↑</option>
+ <option value="price-desc">Price ↓</option>
+ </select>
+ </div>
+ <div class="group"><label for="cols">Density</label>
+ <input id="cols" type="range" min="3" max="8" step="1" value="5">
+ </div>
+ </div>
+
+ <div id="results" class="grid"></div>
+
+ <div class="pager">
+ <button id="prev" disabled>← Previous</button>
+ <span class="info" id="info">—</span>
+ <button id="next" disabled>Next →</button>
+ </div>
+</section>
+
+<footer class="site">
+ <div class="wrap grid-foot">
+ <div>
+ <h4>Walls & Fabrics</h4>
+ <p>A Designer Wallcoverings sister site. Trade orders, memo samples, and quantity quotes available — same catalog, narrower lens.</p>
+ <p class="legal">© <span id="yr"></span> Designer Wallcoverings · 15442 Ventura Bl #102, Sherman Oaks CA 91403</p>
+ </div>
+ <div>
+ <h4>Contact</h4>
+ <p><a href="mailto:info@wallsandfabrics.com">info@wallsandfabrics.com</a></p>
+ <p><a href="tel:+18883734564">888-373-4564</a></p>
+ </div>
+ <div>
+ <h4>Browse</h4>
+ <p><a href="#" data-surface="wallcovering">Wallcoverings</a></p>
+ <p><a href="#" data-surface="fabric">Fabrics</a></p>
+ <p><a href="#" data-surface="mural">Murals</a></p>
+ <p><a href="#" data-surface="trim">Trim</a></p>
+ </div>
+ </div>
+</footer>
+
+<script>
+(function(){
+ // theme toggle
+ var btn=document.getElementById('themeBtn');
+ function setTheme(t){document.documentElement.setAttribute('data-theme',t);try{localStorage.setItem('waf-theme',t);}catch(e){}btn.textContent=(t==='dark')?'☾':'☼';}
+ setTheme(document.documentElement.getAttribute('data-theme')||'light');
+ btn.addEventListener('click',function(){setTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark');});
+
+ // density slider
+ var sl=document.getElementById('cols');
+ function applyCols(v){document.documentElement.style.setProperty('--cols',String(v));try{localStorage.setItem('waf-cols',String(v));}catch(e){}}
+ var savedCols=Number(localStorage.getItem('waf-cols'))||5;
+ sl.value=String(savedCols); applyCols(savedCols);
+ sl.addEventListener('input',function(){applyCols(Number(sl.value));});
+
+ // sort persistence
+ var sortSel=document.getElementById('sort');
+ var savedSort=localStorage.getItem('waf-sort')||'newest';
+ sortSel.value=savedSort;
+ sortSel.addEventListener('change',function(){try{localStorage.setItem('waf-sort',sortSel.value);}catch(e){}reload(1);});
+
+ // search debounce
+ var q=document.getElementById('q'); var qTimer=null;
+ q.addEventListener('input',function(){clearTimeout(qTimer);qTimer=setTimeout(function(){reload(1);},250);});
+
+ // surface tabs
+ var SURFACE_LABELS={all:'All',wallcovering:'Walls',fabric:'Fabrics',mural:'Murals',trim:'Trim'};
+ var state={surface:'all',page:1};
+ var tabs=document.getElementById('surfaceTabs');
+ function buildTabs(counts){
+ var totalAll=Object.values(counts).reduce(function(a,b){return a+b;},0);
+ var keys=['all','wallcovering','fabric','mural','trim'];
+ tabs.innerHTML=keys.map(function(k){
+ var n=k==='all'?totalAll:(counts[k]||0);
+ var label=SURFACE_LABELS[k]||k;
+ return '<button class="surface-tab" data-surface="'+k+'" aria-pressed="'+(state.surface===k?'true':'false')+'">'+label+'<span class="count">'+n.toLocaleString()+'</span></button>';
+ }).join('');
+ Array.from(tabs.querySelectorAll('.surface-tab')).forEach(function(b){
+ b.addEventListener('click',function(){state.surface=b.dataset.surface;buildTabs(counts);reload(1);});
+ });
+ }
+ // nav links + footer browse links also drive surface
+ Array.from(document.querySelectorAll('a[data-surface]')).forEach(function(a){
+ a.addEventListener('click',function(e){e.preventDefault();state.surface=a.dataset.surface;reload(1);document.getElementById('grid').scrollIntoView({behavior:'smooth',block:'start'});});
+ });
+
+ // pager
+ var prev=document.getElementById('prev'),next=document.getElementById('next'),info=document.getElementById('info');
+ prev.addEventListener('click',function(){if(state.page>1){reload(state.page-1);}});
+ next.addEventListener('click',function(){reload(state.page+1);});
+
+ function fmt$(p){if(!p||!p.max_price)return '';var n=Number(p.max_price);return isFinite(n)?'$'+n.toFixed(0):'';}
+ function card(p){
+ var img='<span class="img" style="background-image:url(\\''+(p.image_url||'').replace(/'/g,'%27')+'\\')"></span>';
+ var url=p.product_url||'https://designerwallcoverings.com/products/'+encodeURIComponent(p.handle||p.sku||'');
+ var surface=(p.__surface||'wallcovering');
+ var label=SURFACE_LABELS[surface]||surface;
+ var ttl=(p.title||'(untitled)').replace(/&/g,'&').replace(/</g,'<');
+ var vendor=(p.vendor||'').replace(/&/g,'&').replace(/</g,'<');
+ return '<a href="'+url+'" target="_blank" rel="noopener" class="card" data-sku="'+(p.sku||p.handle||'')+'">'
+ +img
+ +'<div class="body"><div class="surface">'+label+'</div><div class="ttl">'+ttl+'</div>'
+ +'<div class="meta"><span>'+vendor+'</span><span>'+fmt$(p)+'</span></div>'
+ +'<span class="smp">Request memo →</span></div></a>';
+ }
+
+ function renderEmpty(){
+ document.getElementById('results').innerHTML='<div class="empty"><h2>No matches</h2><p>Try widening your search or selecting a different surface.</p></div>';
+ info.textContent='0 results';
+ prev.disabled=true; next.disabled=true;
+ }
+
+ function reload(page){
+ state.page=page||1;
+ var params=new URLSearchParams({sort:sortSel.value,page:String(state.page),limit:'24'});
+ if(q.value.trim()) params.set('q',q.value.trim());
+ if(state.surface && state.surface!=='all') params.set('surface',state.surface);
+ info.textContent='Loading…';
+ fetch('/api/products?'+params.toString()).then(function(r){return r.json();}).then(function(j){
+ buildTabs(j.surfaces||{});
+ if(!j.items || !j.items.length){renderEmpty();return;}
+ document.getElementById('results').innerHTML=j.items.map(card).join('');
+ info.textContent=(j.total.toLocaleString())+' results · page '+j.page+' of '+j.pages;
+ prev.disabled=j.page<=1;
+ next.disabled=j.page>=j.pages;
+ }).catch(function(e){info.textContent='Error loading.';console.error(e);});
+ }
+
+ document.getElementById('yr').textContent=new Date().getFullYear();
+ reload(1);
+})();
+</script>
+
+</body>
+</html>
diff --git a/server.js.pre-sort-skill b/server.js.pre-sort-skill
new file mode 100644
index 0000000..5df1a4b
--- /dev/null
+++ b/server.js.pre-sort-skill
@@ -0,0 +1,159 @@
+/**
+ * Walls & Fabrics — combo storefront for DW wallcoverings + fabrics + murals + trims.
+ * Curated slice of the live Designer Wallcoverings catalog (dw_unified).
+ */
+const express = require('express');
+const helmet = require('helmet');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = process.env.PORT || 9934;
+const DW_SHOPIFY = 'https://designerwallcoverings.com';
+const __SITE = path.basename(__dirname);
+
+let DATA_RAW;
+try {
+ DATA_RAW = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
+ if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
+} catch (e) {
+ console.error(`[${__SITE}] FATAL: could not load products.json — ${e.message}`);
+ DATA_RAW = [];
+}
+
+// Normalize the multiple shapes that ship across DW family datasets:
+// - cork shape: image_url + max_price + product_url
+// - dw_unified shape: images[] + price + dw_url
+function normalize(p) {
+ const out = { ...p };
+ if (!out.image_url) {
+ if (Array.isArray(out.images) && out.images.length) {
+ out.image_url = typeof out.images[0] === 'string' ? out.images[0] : (out.images[0] && out.images[0].src) || '';
+ }
+ }
+ if (out.max_price == null && out.price != null) out.max_price = Number(out.price);
+ if (!out.product_url && out.dw_url) out.product_url = out.dw_url;
+ if (!out.sku && out.handle) out.sku = out.handle;
+ return out;
+}
+
+function isJunk(p) {
+ if (!p.image_url || !String(p.image_url).trim()) return true;
+ if (!p.handle && !p.sku) return true;
+ const t = p.title || '';
+ if (/lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i.test(t)) return true;
+ if (/visual.{0,3}merchandiser/i.test(t)) return true;
+ if (/(?:^|\W)image[ _-]?4(?:\W|$)/i.test(t)) return true;
+ return false;
+}
+
+const PRODUCTS = DATA_RAW.map(normalize).filter(p => !isJunk(p));
+console.log(`[${__SITE}] Loaded ${DATA_RAW.length}, kept ${PRODUCTS.length}, dropped ${DATA_RAW.length - PRODUCTS.length}`);
+
+// Surface tagging — derive a canonical surface bucket per product.
+function surfaceOf(p) {
+ const pt = String(p.product_type || '').toLowerCase();
+ const blob = ((p.title || '') + ' ' + (p.tags || []).join(' ')).toLowerCase();
+ if (pt.includes('fabric')) return 'fabric';
+ if (pt.includes('mural') || /mural/.test(blob)) return 'mural';
+ if (pt.includes('trim') || /trim|tassel|braid|fringe/.test(blob)) return 'trim';
+ return 'wallcovering';
+}
+for (const p of PRODUCTS) p.__surface = surfaceOf(p);
+
+const SURFACE_COUNTS = PRODUCTS.reduce((acc, p) => { acc[p.__surface] = (acc[p.__surface] || 0) + 1; return acc; }, {});
+console.log(`[${__SITE}] Surfaces:`, SURFACE_COUNTS);
+
+const app = express();
+app.use(helmet({ contentSecurityPolicy: false }));
+app.use(express.json({ limit: '256kb' }));
+
+require('./_universal-contact')(app, { siteName: 'Walls & Fabrics', zdColor: '#9A6E3A', zdPosition: 'right' });
+require('./_universal-auth')(app, { siteName: __SITE });
+
+app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
+
+// DW STANDARD: sort by color/style/sku/title — see ~/.claude/CLAUDE.md.
+const COLOR_RANK = {
+ black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
+ red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
+ yellow:30,gold:31,mustard:32,olive:33,
+ green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
+ blue:60,navy:61,indigo:62,periwinkle:63,
+ purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
+};
+function colorRank(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
+ return 999;
+}
+const STYLE_TAGS = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','mid century','rustic','industrial','farmhouse','bohemian','boho','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
+function styleKey(p) {
+ const tags = (p.tags || []).map(t => String(t).toLowerCase());
+ for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
+ return 'zzz';
+}
+function sortProducts(list, mode) {
+ if (mode === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
+ if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
+ if (mode === 'price-asc') return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
+ if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
+ return list; // newest
+}
+
+app.get('/api/products', (req, res) => {
+ const { q, surface, vendor, sort = 'newest', page = 1, limit = 24 } = req.query;
+ let list = PRODUCTS;
+ if (q) {
+ const needle = q.toLowerCase();
+ list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)) || (p.vendor || '').toLowerCase().includes(needle));
+ }
+ if (surface && surface !== 'all') list = list.filter(p => p.__surface === surface);
+ if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+ list = sortProducts(list, sort);
+ const total = list.length;
+ const pageNum = Math.max(1, parseInt(page) || 1);
+ const lim = Math.min(60, parseInt(limit) || 24);
+ const start = (pageNum - 1) * lim;
+ res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total / lim), sort, surfaces: SURFACE_COUNTS, items: list.slice(start, start + lim) });
+});
+
+app.get('/api/facets', (req, res) => {
+ const vendors = {};
+ for (const p of PRODUCTS) vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
+ res.json({ surfaces: SURFACE_COUNTS, vendors, total: PRODUCTS.length });
+});
+
+app.get('/api/sliders', (req, res) => {
+ const out = [];
+ for (const surface of ['wallcovering','fabric','mural','trim']) {
+ const items = PRODUCTS.filter(p => p.__surface === surface).slice(0, 12);
+ if (items.length >= 4) out.push({ surface, items });
+ }
+ res.json({ rails: out });
+});
+
+app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS.length, surfaces: SURFACE_COUNTS }));
+
+app.get('/sample/:handle', (req, res) => {
+ const p = PRODUCTS.find(x => x.handle === req.params.handle || x.sku === req.params.handle);
+ if (!p) return res.status(404).send('Not found');
+ res.redirect(302, p.product_url || `${DW_SHOPIFY}/products/${encodeURIComponent(p.handle)}#sample`);
+});
+
+app.get('/robots.txt', (req, res) => {
+ res.type('text/plain').send(`User-agent: *\nAllow: /\nSitemap: https://wallsandfabrics.com/sitemap.xml\n`);
+});
+app.get('/sitemap.xml', (req, res) => {
+ const urls = ['/'];
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+${urls.map(u => ` <url><loc>https://wallsandfabrics.com${u}</loc><changefreq>weekly</changefreq></url>`).join('\n')}
+</urlset>`;
+ res.type('application/xml').send(xml);
+});
+
+app.listen(PORT, '127.0.0.1', () => {
+ console.log(`[${__SITE}] listening on http://127.0.0.1:${PORT}`);
+});
← 9c75cd7 sort-skill UI: add Light→Dark / Dark→Light / Color Wheel opt
·
back to Wallsandfabrics
·
hamburger nav: apply canonical pattern from sister-site flee 29c57e9 →