[object Object]

← back to Architecturalwallcoverings

fix: /api/facets counts the active filtered set (drill-down) not whole niche catalog; 301 legacy .html paths to clean URLs; sitemap lists canonical clean URLs

6ad1935ca2f3b9caadd004f5db649c6e888d6d24 · 2026-06-01 07:21:58 -0700 · Steve

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

Files touched

Diff

commit 6ad1935ca2f3b9caadd004f5db649c6e888d6d24
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 1 07:21:58 2026 -0700

    fix: /api/facets counts the active filtered set (drill-down) not whole niche catalog; 301 legacy .html paths to clean URLs; sitemap lists canonical clean URLs
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 server.js | 47 +++++++++++++++++++++++++++++++++++------------
 1 file changed, 35 insertions(+), 12 deletions(-)

diff --git a/server.js b/server.js
index 7477348..340aea8 100644
--- a/server.js
+++ b/server.js
@@ -168,6 +168,11 @@ app.use((req, res, next) => {
 });
 // Clean-URL routes for extension-less nav links used across interior pages.
 const CLEAN_URLS = ['care', 'history', 'sourcing', 'trade', 'vocabulary'];
+// Redirect the legacy `.html` form to the canonical clean URL (301) — runs
+// BEFORE express.static so the old extension never serves duplicate content.
+for (const slug of CLEAN_URLS) {
+  app.get('/' + slug + '.html', (req, res) => res.redirect(301, '/' + slug));
+}
 for (const slug of CLEAN_URLS) {
   app.get('/' + slug, (req, res) => res.sendFile(path.join(__dirname, 'public', slug + '.html')));
 }
@@ -175,13 +180,7 @@ app.use(express.static(path.join(__dirname, 'public')));
 
 app.get('/api/products', (req, res) => {
   const { q, aesthetic, vendor, page = 1, limit = 24, sort = 'newest'} = req.query;
-  let list = PRODUCTS_NICHE;
-  if (q) {
-    const needle = q.toLowerCase();
-    list = list.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)));
-  }
-  if (aesthetic && aesthetic !== 'all') list = list.filter(p => p.aesthetic === aesthetic);
-  if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
+  let list = filterProducts(PRODUCTS_NICHE, { q, aesthetic, vendor });
   list = sortProducts(list, sort);
   const total = list.length;
   const pageNum = Math.max(1, parseInt(page) || 1);
@@ -200,13 +199,35 @@ app.get('/api/sliders', (req, res) => {
   res.json({ rails: out });
 });
 
+// Facet counts must reflect the ACTIVE filtered result set (q + cross-facet
+// selections), not the whole niche catalog — drill-down behaviour. Each facet
+// is counted against the set filtered by the OTHER active facets so selecting
+// one facet doesn't zero out the others' options.
+function filterProducts(list, { q, aesthetic, vendor } = {}) {
+  let out = list;
+  if (q) {
+    const needle = String(q).toLowerCase();
+    out = out.filter(p => (p.title || '').toLowerCase().includes(needle) || (p.tags || []).some(t => t.toLowerCase().includes(needle)));
+  }
+  if (aesthetic && aesthetic !== 'all') out = out.filter(p => p.aesthetic === aesthetic);
+  if (vendor && vendor !== 'all') out = out.filter(p => p.vendor === vendor);
+  return out;
+}
+
 app.get('/api/facets', (req, res) => {
+  const { q, aesthetic, vendor } = req.query;
   const aesthetics = {}; const vendors = {};
-  for (const p of PRODUCTS_NICHE) {
+  // Aesthetic counts: filtered by q + vendor (but NOT the aesthetic selection).
+  for (const p of filterProducts(PRODUCTS_NICHE, { q, vendor })) {
     aesthetics[p.aesthetic] = (aesthetics[p.aesthetic] || 0) + 1;
+  }
+  // Vendor counts: filtered by q + aesthetic (but NOT the vendor selection).
+  for (const p of filterProducts(PRODUCTS_NICHE, { q, aesthetic })) {
     vendors[p.vendor] = (vendors[p.vendor] || 0) + 1;
   }
-  res.json({ aesthetics, vendors, total: PRODUCTS_NICHE.length });
+  // total = the fully-filtered set (all active facets applied).
+  const total = filterProducts(PRODUCTS_NICHE, { q, aesthetic, vendor }).length;
+  res.json({ aesthetics, vendors, total });
 });
 
 app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS_NICHE.length, dropped: DROPPED }));
@@ -228,9 +249,11 @@ Sitemap: https://architecturalwallcoverings.com/sitemap.xml
 `);
 });
 app.get('/sitemap.xml', (req, res) => {
-  // Include the real static content pages that exist under public/.
-  const urls = ['/', '/care.html', '/history.html', '/sourcing.html', '/trade.html', '/vocabulary.html']
-    .filter(u => u === '/' || fs.existsSync(path.join(__dirname, 'public', u.slice(1))));
+  // List the clean URLs (canonical) for every static content page that exists
+  // under public/ — the .html form 301s to these, so the sitemap points at the
+  // canonical destination directly.
+  const urls = ['/', ...CLEAN_URLS.map(s => '/' + s)]
+    .filter(u => u === '/' || fs.existsSync(path.join(__dirname, 'public', u.slice(1) + '.html')));
   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://architecturalwallcoverings.com${u}</loc><changefreq>weekly</changefreq></url>`).join('\n')}

← a8da0ee fix: bak-guard regex misses *.bak-* filenames; rm leaked pub  ·  back to Architecturalwallcoverings  ·  fix: add noopener,noreferrer to sister-site window.open popu 14ab3bd →