[object Object]

← back to Wallsandfabrics

untrack .pre-* snapshots and add static 404-guard for .bak/.pre- paths

5152cd85a6cbc7f3a2a9cebfb561a647955770fb · 2026-05-19 15:17:29 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 5152cd85a6cbc7f3a2a9cebfb561a647955770fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 19 15:17:29 2026 -0700

    untrack .pre-* snapshots and add static 404-guard for .bak/.pre- paths
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .gitignore                          |   2 +
 public/index.html.pre-sort-frontend | 267 ------------------------------------
 server.js                           |   6 +
 server.js.pre-sort-skill            | 159 ---------------------
 4 files changed, 8 insertions(+), 426 deletions(-)

diff --git a/.gitignore b/.gitignore
index 646e5f9..bbfbeab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,5 @@ build/
 .next/
 *.bak
 *.bak.*
+*.pre-*
+*.pre
diff --git a/public/index.html.pre-sort-frontend b/public/index.html.pre-sort-frontend
deleted file mode 100644
index c7b8848..0000000
--- a/public/index.html.pre-sort-frontend
+++ /dev/null
@@ -1,267 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width,initial-scale=1">
-<title>Walls &amp; 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">&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>&amp;</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 &amp; 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,'&amp;').replace(/</g,'&lt;');
-    var vendor=(p.vendor||'').replace(/&/g,'&amp;').replace(/</g,'&lt;');
-    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 b/server.js
index 3577140..200a135 100644
--- a/server.js
+++ b/server.js
@@ -75,6 +75,12 @@ app.use(express.json({ limit: '256kb' }));
 require('./_universal-contact')(app, { siteName: 'Walls & Fabrics', zdColor: '#9A6E3A', zdPosition: 'right' });
 require('./_universal-auth')(app, { siteName: __SITE });
 
+// Hard 404-guard — never serve snapshot files (.bak, .bak.*, .pre-*) from static.
+app.use((req, res, next) => {
+  if (/\.(bak|bak\.[^/]+|pre-[^/]*)$/i.test(req.path)) return res.status(404).end();
+  next();
+});
+
 app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
 
 // DW STANDARD: sort by color/style/sku/title — see ~/.claude/CLAUDE.md.
diff --git a/server.js.pre-sort-skill b/server.js.pre-sort-skill
deleted file mode 100644
index 5df1a4b..0000000
--- a/server.js.pre-sort-skill
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * 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}`);
-});

← 449e634 Phase 2: fix card-render syntax error, mobile audit pass  ·  back to Wallsandfabrics  ·  add rel=noreferrer to external product card links 82b7840 →