[object Object]

← back to Bleachresistantfabrics

fix /api/rails facet counts to honor active q/vendor filters via shared filterProducts() drill-down (was counting whole niche catalog)

e8ec1f42bb33acb88288a9d98c100c7b5a85401d · 2026-06-01 07:20:56 -0700 · Steve

Files touched

Diff

commit e8ec1f42bb33acb88288a9d98c100c7b5a85401d
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 1 07:20:56 2026 -0700

    fix /api/rails facet counts to honor active q/vendor filters via shared filterProducts() drill-down (was counting whole niche catalog)
---
 server.js | 31 ++++++++++++++++++++++---------
 1 file changed, 22 insertions(+), 9 deletions(-)

diff --git a/server.js b/server.js
index 87a6b4f..ba47622 100644
--- a/server.js
+++ b/server.js
@@ -116,6 +116,20 @@ function matchesRail(p, key) {
   return re.test(blob);
 }
 
+// Apply the active query/rail/vendor filters to a product list. `skip` lets a
+// facet exclude its own dimension so its counts reflect the rest of the active
+// filter set (standard drill-down facet behaviour), not the whole catalog.
+function filterProducts(list, { q, rail, vendor } = {}, skip = {}) {
+  let out = list;
+  if (q && !skip.q) {
+    const needle = String(q).toLowerCase();
+    out = out.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => String(t).toLowerCase().includes(needle)));
+  }
+  if (rail && rail !== 'all' && RAIL_MATCHERS[rail] && !skip.rail) out = out.filter(p => matchesRail(p, rail));
+  if (vendor && vendor !== 'all' && !skip.vendor) out = out.filter(p => p.vendor === vendor);
+  return out;
+}
+
 const app = express();
 app.use(helmet({ contentSecurityPolicy: false }));
 app.use(express.json({ limit: '256kb' }));
@@ -135,13 +149,7 @@ app.use(express.static(path.join(__dirname, 'public')));
 
 app.get('/api/products', (req, res) => {
   const { q, rail, vendor, page = 1, limit = 24, sort = 'newest' } = 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)));
-  }
-  if (rail && rail !== 'all' && RAIL_MATCHERS[rail]) list = list.filter(p => matchesRail(p, rail));
-  if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+  let list = filterProducts(PRODUCTS, { q, rail, vendor });
   list = sortProducts(list, sort);
   const total = list.length;
   const pageNum = Math.max(1, parseInt(page) || 1);
@@ -151,9 +159,14 @@ app.get('/api/products', (req, res) => {
 });
 
 app.get('/api/rails', (req, res) => {
+  const { q, vendor } = req.query;
+  // Count each rail over the active filter set MINUS the rail dimension itself,
+  // so the numbers tell the user "how many match if I pick this rail" given
+  // their current query/vendor selection — not raw whole-catalog totals.
+  const base = filterProducts(PRODUCTS, { q, vendor }, { rail: true });
   const counts = {};
-  for (const k of Object.keys(RAIL_MATCHERS)) counts[k] = PRODUCTS.filter(p => matchesRail(p, k)).length;
-  res.json({ total: PRODUCTS.length, counts });
+  for (const k of Object.keys(RAIL_MATCHERS)) counts[k] = base.filter(p => matchesRail(p, k)).length;
+  res.json({ total: base.length, counts });
 });
 
 app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS.length }));

← 9001c5d fix: wire IntersectionObserver infinite scroll so all 600 pr  ·  back to Bleachresistantfabrics  ·  Remove Big Red widget — never UX-worked, collided with Help e21c312 →