← back to Philipperomano
Shopify fetch + cache + categorizeTags + safeBodyHtml + memoMailto from kamatera (hunk 3)
00974f4aeb9753d15fc99eea15eaf3318a2a49dd · 2026-05-21 18:56:34 -0700 · Steve Abrams
Files touched
Diff
commit 00974f4aeb9753d15fc99eea15eaf3318a2a49dd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 21 18:56:34 2026 -0700
Shopify fetch + cache + categorizeTags + safeBodyHtml + memoMailto from kamatera (hunk 3)
---
server.js | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 121 insertions(+)
diff --git a/server.js b/server.js
index d3e6d53..39b500a 100644
--- a/server.js
+++ b/server.js
@@ -6,6 +6,7 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
+const https = require('https');
const PORT = process.env.PORT || 9831;
const DATA_FILE = path.join(__dirname, 'data', 'products.json');
@@ -287,6 +288,126 @@ function memoSampleUrl(handle) {
return handle ? `${DW_SHOPIFY}/products/${handle}#sample` : DW_SHOPIFY;
}
+// Lazy-fetch + cache the full Shopify product per handle, so the detail page
+// can mirror everything that's on the canonical DW Shopify product page
+// (description, price, variants, images, tags) without re-fetching on each request.
+const productCache = new Map();
+const PRODUCT_TTL_MS = 60 * 60 * 1000; // 1h
+function fetchShopifyProduct(handle) {
+ const cached = productCache.get(handle);
+ if (cached && Date.now() - cached.at < PRODUCT_TTL_MS) return Promise.resolve(cached.data);
+ return new Promise((resolve) => {
+ const opts = {
+ hostname: 'designerwallcoverings.com',
+ path: `/products/${encodeURIComponent(handle)}.json`,
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (compatible; PhillipeRomanoSite/1.0; +https://philipperomano.com)',
+ 'Accept': 'application/json'
+ }
+ };
+ https.get(opts, (r) => {
+ let body = '';
+ r.on('data', c => body += c);
+ r.on('end', () => {
+ try {
+ const data = JSON.parse(body)?.product || null;
+ if (data) productCache.set(handle, { data, at: Date.now() });
+ resolve(data);
+ } catch { resolve(null); }
+ });
+ }).on('error', () => resolve(null));
+ });
+}
+function fetchVariantId(handle) {
+ return fetchShopifyProduct(handle).then(p => p?.variants?.[0]?.id || null);
+}
+
+// Group product tags into meaningful sections for the detail page.
+// Hides internal/utility tags (lowercase mfr codes, AI-* flags) and groups the rest.
+function categorizeTags(tagsStr) {
+ if (!tagsStr) return {};
+ const tags = tagsStr.split(',').map(t => t.trim()).filter(Boolean);
+ const ROOMS = /^(Bedroom|Living Room|Dining Room|Hallway|Bathroom|Kitchen|Office|Foyer|Entryway|Powder Room|Nursery|Family Room|Hospitality|Restaurant|Hotel|Lobby|Healthcare|Retail|Spa)$/i;
+ const STYLES = /^(Contemporary|Traditional|Transitional|Modern|Organic Modern|Mid-Century|Mid Century|Classic|Bohemian|Minimalist|Rustic|Industrial|Coastal|Farmhouse|Glam|Art Deco|Eclectic|Scandinavian|Japandi|Maximalist|Old World)$/i;
+ const MOODS = /^(Serene|Calming|Dramatic|Bold|Subtle|Elegant|Romantic|Sophisticated|Energetic|Cozy|Luxurious|Playful|Refined|Warm)$/i;
+ const MATERIALS = /^(Grasscloth|Linen|Velvet|Silk|Cotton|Wool|Vinyl|Performance Vinyl|Faux Leather|Faux-Leather|Leather|Paper|Mylar|Mica|Cork|Hemp|Jute|Sisal|Raffia|Abaca|Beaded|Sequin|Glass Bead|Flock|Flocked|Embroidered|Hand-Painted|Acoustic)$/i;
+ const TEXTURES = /Texture|Weave|Tweed|Boucle|Plaid|Stripe|Damask|Chevron|Geometric|Floral|Botanical|Animal|Trellis|Lattice|Diamond|Houndstooth|Plain|Solid/i;
+ const APPS = /^(Architectural|Commercial|Hospitality|Residential|Class A Fire Rated|Class A|Class B|Class C|Type II|Type III|Heavy Duty|Acoustic Rated|FR|Fire Rated|Wipeable|Scrubbable|Bleach Cleanable|Mildew Resistant|Stain Resistant)$/i;
+ const COLLECTIONS = /^(Trending|Best Seller|New Arrival|Limited Edition).*$/i;
+ const COLOR_PREFIX = /^Color\s*:\s*/i;
+ const CERTS = /Fire Rated|Class [A-Z]|Type II|Type III|Heavy Duty/i;
+
+ // Skip patterns: lowercase-only utility tags, mfr SKUs/handles, AI flags
+ const SKIP = (t) => (
+ /^[a-z][a-z0-9-]*$/.test(t) || // all-lowercase tokens (mfr-handles, sku slugs)
+ /^AI-/i.test(t) ||
+ /^Phillipe Romano$/i.test(t) ||
+ /^decor wallcovering$/i.test(t) ||
+ /^performance-vinyl$/i.test(t) ||
+ /^vinyl$/i.test(t) && tags.includes('Vinyl')
+ );
+
+ const groups = {
+ colors: new Set(),
+ style: new Set(),
+ mood: new Set(),
+ rooms: new Set(),
+ materials: new Set(),
+ textures: new Set(),
+ certifications: new Set(),
+ application: new Set(),
+ collection: new Set(),
+ other: new Set()
+ };
+
+ for (const t of tags) {
+ if (SKIP(t)) continue;
+ if (COLOR_PREFIX.test(t)) {
+ groups.colors.add(t.replace(COLOR_PREFIX, '').trim());
+ continue;
+ }
+ if (CERTS.test(t)) { groups.certifications.add(t); continue; }
+ if (ROOMS.test(t)) { groups.rooms.add(t); continue; }
+ if (STYLES.test(t)) { groups.style.add(t); continue; }
+ if (MOODS.test(t)) { groups.mood.add(t); continue; }
+ if (MATERIALS.test(t)) { groups.materials.add(t); continue; }
+ if (APPS.test(t)) { groups.application.add(t); continue; }
+ if (COLLECTIONS.test(t)) { groups.collection.add(t); continue; }
+ if (TEXTURES.test(t)) { groups.textures.add(t); continue; }
+ // Title-cased single-word colorways → colors heuristic
+ if (/^[A-Z][a-z]+$/.test(t) && t.length <= 12) { groups.colors.add(t); continue; }
+ groups.other.add(t);
+ }
+ // To array
+ const out = {};
+ for (const [k, v] of Object.entries(groups)) {
+ const arr = [...v];
+ if (arr.length) out[k] = arr;
+ }
+ return out;
+}
+
+// Sanitize body_html: allow paragraphs, lists, line-breaks, basic emphasis. Strip scripts/styles/iframes.
+function safeBodyHtml(html) {
+ if (!html) return '';
+ return String(html)
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
+ .replace(/<iframe[\s\S]*?<\/iframe>/gi, '')
+ .replace(/\son\w+="[^"]*"/gi, '')
+ .replace(/\son\w+='[^']*'/gi, '');
+}
+
+function memoMailto(p) {
+ const sub = encodeURIComponent(`Memo Sample Request — ${p.dw_sku || p.title}`);
+ const body = encodeURIComponent(
+ `Hi Phillipe Romano,\n\nI'd like to request a free memo sample of:\n\n` +
+ ` ${p.title}\n SKU: ${p.dw_sku || ''}\n\n` +
+ `Please ship to:\n Name:\n Company:\n Address:\n City, State, ZIP:\n Phone:\n\nThank you.`
+ );
+ return `mailto:${PR_EMAIL}?subject=${sub}&body=${body}`;
+}
+
const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
<html lang="en">
<head>
← 30d1a86 color buckets + colorway-hex + ENRICHED + junk-filter from k
·
back to Philipperomano
·
header.top flex + .contact CSS from kamatera (hunk 4) 8fdcf2f →