[object Object]

← back to Philipperomano

naturals: add /naturals route + nav link + inclusive classifier

fe6a21f8f6599f7ffe5751f91d7ca7b35ed0d4e2 · 2026-05-25 00:26:25 -0700 · Steve Abrams

Catches all 1,894 natural-fiber wallcoverings — both real fiber (grasscloth /
cork / linen / silk / jute / sisal / abaca / raffia / seagrass / bamboo) AND
Type II commercial vinyls with natural-look textures (linen-look / cork-look /
grasscloth-look / etc.).

Per Steve's directive 2026-05-24 — "most Type II vinyl textures work". The
classifier is intentionally inclusive: any product whose product_type contains
'natural' OR whose title/handle matches the NATURALS_RE regex (which includes
all the fiber names PLUS texture-suggestion words like 'fiber', 'woven',
'paperweave', 'cane', 'rattan', 'reed', 'sandstone') lands in /naturals.

Changes:
- ENRICHED rows now carry _isNatural boolean (computed on boot, ~1ms total)
- NEW NATURALS_COUNT constant, printed in boot log
- NEW /naturals route — 302-redirects to /?cat=naturals
- Index route now reads ?cat= alongside ?type=, filters ENRICHED (not raw DATA)
- Nav: 'Naturals (1,894)' link with count, between 'All' and the product-type list
- layout() takes a cat= param so the active highlight is correct

Boot results — junk-filter keeps 5,913 of 8,227 (drops 2,314), naturals
enriched count = 1,894 (32% of post-filter catalog).

Files touched

Diff

commit fe6a21f8f6599f7ffe5751f91d7ca7b35ed0d4e2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 25 00:26:25 2026 -0700

    naturals: add /naturals route + nav link + inclusive classifier
    
    Catches all 1,894 natural-fiber wallcoverings — both real fiber (grasscloth /
    cork / linen / silk / jute / sisal / abaca / raffia / seagrass / bamboo) AND
    Type II commercial vinyls with natural-look textures (linen-look / cork-look /
    grasscloth-look / etc.).
    
    Per Steve's directive 2026-05-24 — "most Type II vinyl textures work". The
    classifier is intentionally inclusive: any product whose product_type contains
    'natural' OR whose title/handle matches the NATURALS_RE regex (which includes
    all the fiber names PLUS texture-suggestion words like 'fiber', 'woven',
    'paperweave', 'cane', 'rattan', 'reed', 'sandstone') lands in /naturals.
    
    Changes:
    - ENRICHED rows now carry _isNatural boolean (computed on boot, ~1ms total)
    - NEW NATURALS_COUNT constant, printed in boot log
    - NEW /naturals route — 302-redirects to /?cat=naturals
    - Index route now reads ?cat= alongside ?type=, filters ENRICHED (not raw DATA)
    - Nav: 'Naturals (1,894)' link with count, between 'All' and the product-type list
    - layout() takes a cat= param so the active highlight is correct
    
    Boot results — junk-filter keeps 5,913 of 8,227 (drops 2,314), naturals
    enriched count = 1,894 (32% of post-filter catalog).
---
 server.js | 40 +++++++++++++++++++++++++++++++++++-----
 1 file changed, 35 insertions(+), 5 deletions(-)

diff --git a/server.js b/server.js
index 04ba2f0..84c510f 100644
--- a/server.js
+++ b/server.js
@@ -214,6 +214,21 @@ function effectiveSku(p) {
   const m = (p.handle || '').match(/(dw[a-z]{2,4}-\d+)$/i);
   return m ? m[1].toUpperCase() : '';
 }
+// Inclusive naturals matcher — catches BOTH real natural-fiber wallcoverings
+// (grasscloth, cork, linen, silk, jute, sisal, abaca, raffia, seagrass, bamboo,
+// hemp, flax, wool, leather, suede, mica, capiz) AND Type II commercial vinyls
+// with natural-look textures (linen-look, grasscloth-look, cork-look, etc.).
+// Per Steve 2026-05-25 — "most Type II vinyl textures work".
+const NATURALS_RE = /\b(grasscloth|seagrass|sea[- ]grass|jute|sisal|abaca|raffia|cork|linen|silk|silk[- ]?look|bamboo|hemp|flax|wool|leather|suede|mica|capiz|slate|natural|naturals|fiber|woven|sandstone|reed|cane|rattan|paperweave|paper[- ]weave|sandgrass)\b/i;
+function isNatural(p) {
+  // product_type is the strongest signal when present
+  const pt = (p.product_type || '').toLowerCase();
+  if (pt.includes('natural')) return true;
+  // Title-keyword pattern (catches Type II naturals-look vinyls)
+  const blob = (p.title || '') + ' ' + (p.handle || '');
+  return NATURALS_RE.test(blob);
+}
+
 const ENRICHED = DATA.map(p => {
   const sku = effectiveSku(p);
   return {
@@ -221,7 +236,8 @@ const ENRICHED = DATA.map(p => {
     dw_sku: sku || p.dw_sku,
     _color: colorBucket(p.title),
     _hex: colorHex(p.title),
-    _skuNum: skuNum(sku)
+    _skuNum: skuNum(sku),
+    _isNatural: isNatural(p),
   };
 });
 // Default order: newest first (highest DWxx sku numeric desc)
@@ -256,6 +272,8 @@ ENRICHED.push(..._kept);
 
 const COLOR_COUNTS = {};
 for (const p of ENRICHED) if (p._color) COLOR_COUNTS[p._color] = (COLOR_COUNTS[p._color] || 0) + 1;
+const NATURALS_COUNT = ENRICHED.filter(p => p._isNatural).length;
+console.log(`[naturals] enriched count: ${NATURALS_COUNT} of ${ENRICHED.length}`);
 
 const esc = s => String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
 
@@ -408,7 +426,7 @@ function memoMailto(p) {
   return `mailto:${PR_EMAIL}?subject=${sub}&body=${body}`;
 }
 
-const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
+const layout = (title, body, { q='', type='', cat='' } = {}) => `<!doctype html>
 <html lang="en">
 <head>
 <meta charset="utf-8">
@@ -491,7 +509,8 @@ const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
   <a href="/" class="brand">Phillipe Romano<small>A Designer Wallcoverings Exclusive</small></a>
 </header>
 <nav class="types">
-  <a href="/" ${!type ? 'class="active"' : ''}>All</a>
+  <a href="/" ${!type && !cat ? 'class="active"' : ''}>All</a>
+  <a href="/naturals" ${cat==='naturals' ? 'class="active"' : ''}>Naturals (${NATURALS_COUNT.toLocaleString()})</a>
   ${TYPES.map(t => `<a href="/?type=${encodeURIComponent(t)}" ${type===t ? 'class="active"' : ''}>${esc(t)}</a>`).join('')}
   <form class="search" method="GET" action="/">
     <input name="q" value="${esc(q)}" placeholder="Search 8,227 SKUs…">
@@ -506,13 +525,24 @@ ${body}
 </body>
 </html>`;
 
+// /naturals — friendly shortcut → /?cat=naturals
+app.get('/naturals', (req, res) => {
+  const u = new URL('http://x' + req.url);
+  u.searchParams.set('cat', 'naturals');
+  u.pathname = '/';
+  res.redirect(302, '/?' + u.searchParams.toString());
+});
+
 app.get('/', (req, res) => {
   const q = (req.query.q || '').toString().toLowerCase().trim();
   const type = (req.query.type || '').toString();
+  const cat  = (req.query.cat  || '').toString().toLowerCase();
   const sort = (req.query.sort || '').toString();
   const page = parseInt(req.query.page, 10) || 1;
 
-  let filtered = DATA;
+  // Source from ENRICHED (junk-filtered + _isNatural flag set) — not raw DATA
+  let filtered = ENRICHED;
+  if (cat === 'naturals') filtered = filtered.filter(p => p._isNatural);
   if (type) filtered = filtered.filter(p => NORM_TYPE(p.product_type) === type);
   if (q) {
     const tokens = q.split(/\s+/);
@@ -613,7 +643,7 @@ app.get('/', (req, res) => {
       })();
     </script>
   `;
-  res.set('Content-Type','text/html').send(layout(`${type || 'All Products'}${q ? ` · ${q}` : ''}`, body, { q, type }));
+  res.set('Content-Type','text/html').send(layout(`${cat === 'naturals' ? 'Naturals' : (type || 'All Products')}${q ? ` · ${q}` : ''}`, body, { q, type, cat }));
 });
 
 app.get('/p/:handle', (req, res) => {

← 8fdcf2f header.top flex + .contact CSS from kamatera (hunk 4)  ·  back to Philipperomano  ·  scrub vendor name leaks from products.json source (176 produ cf04469 →