← 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 →