← 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 →