[object Object]

← back to Raffiawallcoverings

server.js mechanical fixes: (1) /api/facets drill-down — aesthetics counted over the q-filtered set (not the aesthetic-filtered one), vendors over q+aesthetic, total = fully filtered; (2) 301 redirect legacy /(history|vocabulary|sourcing|care|trade|index).html -> clean URLs before express.static (was serving both forms = duplicate content)

638b7220e50b5e4de6da47751fe794ae38ca0adf · 2026-06-01 07:41:35 -0700 · steve

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

Files touched

Diff

commit 638b7220e50b5e4de6da47751fe794ae38ca0adf
Author: steve <steve@designerwallcoverings.com>
Date:   Mon Jun 1 07:41:35 2026 -0700

    server.js mechanical fixes: (1) /api/facets drill-down — aesthetics counted over the q-filtered set (not the aesthetic-filtered one), vendors over q+aesthetic, total = fully filtered; (2) 301 redirect legacy /(history|vocabulary|sourcing|care|trade|index).html -> clean URLs before express.static (was serving both forms = duplicate content)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 server.js | 40 +++++++++++++++++++++++++++++-----------
 1 file changed, 29 insertions(+), 11 deletions(-)

diff --git a/server.js b/server.js
index 0bce6f8..0dcc8d6 100644
--- a/server.js
+++ b/server.js
@@ -147,6 +147,13 @@ app.use(express.json({ limit: '256kb' }));
 require('./_universal-contact')(app, { siteName: "Raffia Wallcoverings", zdColor: "#a87830", zdPosition: 'right' });
 require('./_universal-auth')(app, { siteName: "raffiawallcoverings" });
 
+// 301 legacy .html -> clean URL (must precede express.static so the .html form
+// isn't served raw = duplicate content). index.html stays at / via static.
+app.get(/^\/(history|vocabulary|sourcing|care|trade|index)\.html$/, (req, res) => {
+  const slug = req.path.slice(1).replace(/\.html$/, '');
+  res.redirect(301, slug === 'index' ? '/' : '/' + slug);
+});
+
 app.use(express.static(path.join(__dirname, 'public')));
 
 app.get('/api/products', (req, res) => {
@@ -176,21 +183,32 @@ app.get('/api/sliders', (req, res) => {
 });
 
 app.get('/api/facets', (req, res) => {
-  // total counts the FILTERED list (same filters as /api/products) so the
-  // header "N Patterns." reflects the current search/facet, not the catalog total.
+  // Drill-down counts: each facet dimension is counted over the set filtered by
+  // the OTHER active facets (so picking aesthetic=X still shows the counts of the
+  // other aesthetics you could switch to), and total = the fully-filtered set —
+  // matching the header "N Patterns." to the current search/facet, not the catalog.
   const { q, aesthetic } = req.query;
-  let list = PRODUCTS_NICHE;
-  if (q) {
+  const byQ = (p) => {
+    if (!q) return true;
     const needle = String(q).toLowerCase();
-    list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)));
+    return (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle));
+  };
+  const byAesthetic = (p) => !aesthetic || aesthetic === 'all' || p.aesthetic === aesthetic;
+
+  // aesthetics dimension: count over everything EXCEPT the aesthetic filter
+  const aesthetics = {};
+  for (const p of PRODUCTS_NICHE) {
+    if (byQ(p)) aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
   }
-  if (aesthetic && aesthetic !== 'all') list = list.filter(p => p.aesthetic === aesthetic);
-  const aesthetics = {}; const vendors = {};
-  for (const p of list) {
-    aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
-    vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
+  // vendors dimension: count over q + aesthetic (vendor is its own dimension)
+  const vendors = {};
+  for (const p of PRODUCTS_NICHE) {
+    if (byQ(p) && byAesthetic(p)) vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
   }
-  res.json({ aesthetics, vendors, total: list.length });
+  // total: the fully-filtered set (same predicates as /api/products)
+  const total = PRODUCTS_NICHE.filter(p => byQ(p) && byAesthetic(p)).length;
+
+  res.json({ aesthetics, vendors, total });
 });
 
 app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS_NICHE.length, dropped: DROPPED }));

← f62cb8c remove public/index.html.bak-20260526152130 (was served by e  ·  back to Raffiawallcoverings  ·  noopener,noreferrer on constellation window.open popup (FB t dcda1f6 →