← back to Hollywood Wallcoverings
Untrack backup snapshot, broaden .gitignore, add .bak/.pre- 404-guard
2befa8b8bc7609bed97123361bb3cf185dca8fb0 · 2026-05-19 08:00:45 -0700 · Steve Abrams
server.js.bak.nshdr.1778214775073 was committed despite *.bak in
.gitignore (ignore doesn't untrack). Removed from index, kept on disk.
.gitignore now covers *.bak.* and *.pre-* too. Added an Express
middleware that 404s any path containing .bak or .pre- so a stray
snapshot can never be served from a static root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M .gitignoreM server.jsD server.js.bak.nshdr.1778214775073
Diff
commit 2befa8b8bc7609bed97123361bb3cf185dca8fb0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 19 08:00:45 2026 -0700
Untrack backup snapshot, broaden .gitignore, add .bak/.pre- 404-guard
server.js.bak.nshdr.1778214775073 was committed despite *.bak in
.gitignore (ignore doesn't untrack). Removed from index, kept on disk.
.gitignore now covers *.bak.* and *.pre-* too. Added an Express
middleware that 404s any path containing .bak or .pre- so a stray
snapshot can never be served from a static root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
.gitignore | 2 +
server.js | 10 +
server.js.bak.nshdr.1778214775073 | 543 --------------------------------------
3 files changed, 12 insertions(+), 543 deletions(-)
diff --git a/.gitignore b/.gitignore
index 7e6a9c3..1140724 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ dist/
build/
.next/
*.bak
+*.bak.*
+*.pre-*
diff --git a/server.js b/server.js
index c047a49..c126fb1 100644
--- a/server.js
+++ b/server.js
@@ -36,6 +36,16 @@ app.use((req, res, next) => {
}
next();
});
+// 404-guard — snapshot/backup files must never serve, even if dropped into a
+// static root. Blocks any path containing .bak or .pre- (e.g. server.js.bak,
+// foo.bak.123, index.html.pre-deploy).
+app.use((req, res, next) => {
+ if (/\.bak(\.|$|\/)|\.pre-/i.test(req.path)) {
+ return res.status(404).type('text/plain').send('Not found');
+ }
+ next();
+});
+
const NORM_TYPE = (t) => {
if (!t) return 'Other';
const x = t.replace(/s$/i, '').trim();
diff --git a/server.js.bak.nshdr.1778214775073 b/server.js.bak.nshdr.1778214775073
deleted file mode 100644
index 93b88a0..0000000
--- a/server.js.bak.nshdr.1778214775073
+++ /dev/null
@@ -1,543 +0,0 @@
-/**
- * hollywoodwallcoverings.com — Hollywood Wallcoverings storefront.
- * Reads products.json, exposes search/filter/pagination, links memo-sample CTA
- * back to the DW Shopify product page (#sample anchor).
- */
-const express = require('express');
-const helmet = require('helmet');
-const path = require('path');
-const fs = require('fs');
-
-const PORT = process.env.PORT || 9832;
-const __SITE = path.basename(__dirname);
-let DATA_RAW;
-try {
- const raw = fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8');
- DATA_RAW = JSON.parse(raw);
- if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
-} catch (e) {
- console.error(`[${__SITE}] FATAL: could not load products.json — ${e.message}`);
- console.error(`[${__SITE}] Starting with empty catalog. Run pull script to populate data/products.json.`);
- DATA_RAW = [];
-}
-const DW_SHOPIFY = 'https://designerwallcoverings.com';
-
-const app = express();
-
-// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
-app.use(helmet({ contentSecurityPolicy: false }));
-
-// Disable Cloudflare HTML caching — origin must say no-store so edits propagate
-// immediately. CF respects no-store; max-age=0 alone is still cacheable.
-// (added 2026-05-07 fleet cache-header sweep)
-app.use((req, res, next) => {
- if (!req.path.startsWith('/api/')) {
- res.set('Cache-Control', 'no-store, must-revalidate');
- }
- next();
-});
-const NORM_TYPE = (t) => {
- if (!t) return 'Other';
- const x = t.replace(/s$/i, '').trim();
- if (/wallcovering/i.test(x)) return 'Wallcovering';
- if (/natural/i.test(x)) return 'Natural';
- if (/drapery/i.test(x)) return 'Drapery';
- if (/fabric/i.test(x)) return 'Fabric';
- return x;
-};
-
-// Junk filter — drop products that don't belong on this storefront
-function isJunk(p) {
- if (!p.image_url || !p.image_url.trim()) return true;
- if (!p.dw_sku || !p.dw_sku.trim()) return true;
- if (/^\d{13,}$/.test(p.dw_sku)) return true; // variant_id leak
- if (/lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i.test(p.title || '')) return true;
- const t = p.product_type || '';
- if (!/wallcovering|fabric/i.test(t)) return true;
- return false;
-}
-
-const DATA = DATA_RAW.filter(p => !isJunk(p));
-const DROPPED = DATA_RAW.length - DATA.length;
-console.log(`Loaded ${DATA_RAW.length} products, kept ${DATA.length}, dropped ${DROPPED} as junk`);
-
-const TYPES = [...new Set(DATA.map(p => NORM_TYPE(p.product_type)))].sort();
-
-const esc = s => String(s||'').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
-
-function paginate(arr, page, perPage = 60) {
- const total = arr.length;
- const pages = Math.max(1, Math.ceil(total / perPage));
- const p = Math.min(Math.max(1, page), pages);
- return { items: arr.slice((p-1)*perPage, p*perPage), page: p, pages, total };
-}
-
-// JSON endpoint for infinite scroll
-function filterAndPaginate(req, perPage = 48) {
- const q = (req.query.q || '').toString().toLowerCase().trim();
- const type = (req.query.type || '').toString();
- const page = parseInt(req.query.page) || 1;
- let filtered = DATA;
- if (type) filtered = filtered.filter(p => NORM_TYPE(p.product_type) === type);
- if (q) {
- const tokens = q.split(/\s+/);
- filtered = filtered.filter(p => {
- const hay = `${p.title} ${p.dw_sku||''} ${p.mfr_sku||''}`.toLowerCase();
- return tokens.every(t => hay.includes(t));
- });
- }
- const pages = Math.max(1, Math.ceil(filtered.length / perPage));
- const p = Math.min(Math.max(1, page), pages);
- return { items: filtered.slice((p-1)*perPage, p*perPage), page: p, pages, total: filtered.length };
-}
-
-function shopifyUrl(handle) {
- return handle ? `${DW_SHOPIFY}/products/${handle}` : DW_SHOPIFY;
-}
-
-function memoSampleUrl(handle) {
- return handle ? `${DW_SHOPIFY}/products/${handle}#sample` : DW_SHOPIFY;
-}
-
-// Hollywood = cinematic, brass + ivory + deep noir. Old-Hollywood glamour tone.
-const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width,initial-scale=1">
-<title>${esc(title)} — Hollywood Wallcoverings</title>
-<meta name="description" content="Hollywood Wallcoverings — commercial-grade Type II vinyls, naturals, and fabrics for interior designers. Free memo samples and trade-only pricing.">
-<meta property="og:title" content="${esc(title)} · Hollywood Wallcoverings">
-<meta property="og:description" content="Old-Hollywood glamour for the modern interior. Free memo samples · trade pricing · hand-selected for designers.">
-<style>
- :root {
- --bg:#0e0a06;
- --bg-card:#1a130c;
- --bg-soft:#241a10;
- --fg:#f6efe1;
- --muted:#a89880;
- --rule:#3a2c1d;
- --accent:#c9a865; /* brass */
- --accent-soft:#e0c896; /* champagne */
- --accent-dim:#7d6231;
- --serif:'Cormorant Garamond', 'Playfair Display', Georgia, serif;
- --display:'Cinzel', 'Cormorant Garamond', Georgia, serif;
- --sans: -apple-system, 'Inter', 'Segoe UI', system-ui, sans-serif;
- }
- *{box-sizing:border-box;margin:0;padding:0}
- html,body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5}
- a{color:inherit;text-decoration:none}
- header.top{padding:32px 32px 24px;border-bottom:1px solid var(--rule);background:linear-gradient(180deg, #1a130c 0%, #0e0a06 100%);text-align:center;position:relative}
- .hamburger{position:absolute;top:32px;right:32px;background:none;border:1px solid var(--accent-dim);color:var(--accent);width:40px;height:40px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:border-color .15s, background .15s}
- .hamburger:hover{border-color:var(--accent);background:rgba(201,168,101,.08)}
- .hamburger span{display:block;width:18px;height:1.5px;background:var(--accent);transition:transform .25s, opacity .25s}
- .hamburger.open span:nth-child(1){transform:translateY(5.5px) rotate(45deg)}
- .hamburger.open span:nth-child(2){opacity:0}
- .hamburger.open span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg)}
- .menu-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:90;opacity:0;pointer-events:none;transition:opacity .25s}
- .menu-overlay.open{opacity:1;pointer-events:auto}
- .menu-panel{position:fixed;top:0;right:0;height:100vh;width:360px;max-width:90vw;background:var(--bg-card);border-left:1px solid var(--rule);z-index:100;transform:translateX(100%);transition:transform .3s ease;padding:80px 36px 36px;overflow-y:auto}
- .menu-panel.open{transform:translateX(0)}
- .menu-panel h3{font-family:var(--display);font-size:14px;letter-spacing:.24em;color:var(--accent);text-transform:uppercase;margin-bottom:28px;font-weight:600}
- .menu-panel ul{list-style:none;padding:0;margin:0}
- .menu-panel li{margin-bottom:0;border-bottom:1px solid var(--rule)}
- .menu-panel li:last-child{border-bottom:none}
- .menu-panel a{display:block;padding:16px 0;font-family:var(--serif);font-size:20px;color:var(--fg);transition:color .15s,padding-left .15s}
- .menu-panel a:hover{color:var(--accent);padding-left:8px}
- .menu-panel .menu-foot{margin-top:48px;padding-top:24px;border-top:1px solid var(--rule);font-size:11px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase;line-height:1.8}
- .menu-panel .menu-foot a{font-family:var(--sans);font-size:11px;display:inline;color:var(--accent-soft);padding:0}
- .brand{font-family:var(--display);font-weight:600;font-size:38px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent)}
- .brand small{display:block;font-family:var(--sans);font-size:11px;letter-spacing:.32em;font-weight:500;color:var(--muted);margin-top:6px;text-transform:uppercase}
- nav.types{display:flex;gap:22px;padding:14px 32px;background:var(--bg-card);border-bottom:1px solid var(--rule);flex-wrap:wrap;align-items:center}
- nav.types a{font-size:12px;font-weight:500;color:var(--muted);padding:6px 0;border-bottom:2px solid transparent;letter-spacing:.14em;text-transform:uppercase;transition:color .15s, border-color .15s}
- nav.types a:hover{color:var(--accent-soft)}
- nav.types a.active{color:var(--accent);border-bottom-color:var(--accent)}
- form.search{margin-left:auto;display:flex;gap:8px;align-items:center}
- form.search input{border:1px solid var(--rule);background:var(--bg);padding:9px 14px;border-radius:0;min-width:260px;font:inherit;font-size:13px;outline:none;color:var(--fg);font-family:var(--sans)}
- form.search input:focus{border-color:var(--accent)}
- form.search input::placeholder{color:var(--muted);font-style:italic}
- form.search button{border:1px solid var(--accent);background:transparent;color:var(--muted, var(--accent));padding:9px 18px;font:inherit;font-size:11px;text-transform:uppercase;letter-spacing:.16em;font-weight:600;cursor:pointer;transition:background .15s, color .15s}
- form.search button:hover{background:var(--accent);color:var(--bg)}
- main{max-width:1400px;margin:0 auto;padding:36px 32px}
- .controls-bar{display:flex;align-items:center;gap:18px;margin-bottom:18px;flex-wrap:wrap;justify-content:flex-end}
- .controls-bar label{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);font-weight:500}
- .controls-bar select{background:transparent;border:1px solid var(--rule);color:var(--fg);padding:7px 12px;font:inherit;font-size:12px;letter-spacing:.10em;text-transform:uppercase;cursor:pointer;outline:none}
- .controls-bar select:focus{border-color:var(--accent)}
- .controls-bar input[type=range]{accent-color:var(--accent);width:140px;cursor:pointer}
- .meta-line{font-size:11px;color:var(--muted);margin-bottom:24px;letter-spacing:.16em;text-transform:uppercase;text-align:center;font-weight:500}
- .meta-line strong{color:var(--accent-soft);font-weight:600}
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:28px}
- .card{background:var(--bg-card);border:1px solid var(--rule);transition:box-shadow .2s, transform .15s, border-color .2s;overflow:hidden}
- .card:hover{box-shadow:0 12px 40px rgba(0,0,0,.5), 0 0 0 1px var(--accent-dim);transform:translateY(-3px);border-color:var(--accent-dim)}
- .card a.thumb{display:block;aspect-ratio:1/1;background:#0a0703;overflow:hidden;position:relative}
- .card a.thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .4s ease}
- .card:hover a.thumb img{transform:scale(1.04)}
- .card .info{padding:16px 16px 14px}
- .card .info .t{font-family:var(--serif);font-size:18px;font-weight:500;line-height:1.25;margin-bottom:6px;color:var(--fg)}
- .card .info .sub{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.12em}
- .card .actions{display:flex;border-top:1px solid var(--rule)}
- .card .actions a{flex:1;padding:11px 12px;text-align:center;font-size:10px;text-transform:uppercase;letter-spacing:.14em;font-weight:500;color:var(--muted);transition:all .15s}
- .card .actions a + a{border-left:1px solid var(--rule)}
- .card .actions a:hover{background:var(--bg-soft);color:var(--accent-soft)}
- .card .actions a.cta{color:var(--accent);font-weight:600}
- .card .actions a.cta:hover{background:var(--accent);color:var(--bg)}
- .pager{display:flex;justify-content:center;gap:8px;margin-top:56px}
- .pager a, .pager span{padding:9px 16px;border:1px solid var(--rule);background:var(--bg-card);font-size:12px;min-width:44px;text-align:center;letter-spacing:.06em}
- .pager a:hover{border-color:var(--accent);color:var(--accent)}
- .pager .cur{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
- .empty{padding:80px 20px;text-align:center;color:var(--muted);font-style:italic}
- footer{padding:48px 32px;border-top:1px solid var(--rule);background:var(--bg-card);color:var(--muted);font-size:11px;letter-spacing:.14em;text-align:center;margin-top:96px;text-transform:uppercase}
- footer a{color:var(--accent)}
- footer a:hover{color:var(--accent-soft)}
- .footer-tag{font-family:var(--serif);font-style:italic;text-transform:none;letter-spacing:.02em;font-size:14px;color:var(--accent-soft);margin-bottom:14px;display:block}
- /* Detail page */
- .detail{display:grid;grid-template-columns:1.2fr 1fr;gap:56px;padding:56px 32px;max-width:1400px;margin:0 auto}
- .detail .hero{background:#0a0703;aspect-ratio:1/1;border:1px solid var(--rule)}
- .detail .hero img{width:100%;height:100%;object-fit:cover;display:block}
- .detail h1{font-family:var(--serif);font-size:52px;font-weight:500;line-height:1.05;margin-bottom:12px;color:var(--fg)}
- .detail .crumb{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:18px}
- .detail .crumb a:hover{color:var(--accent)}
- .detail .pt{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);font-weight:600;margin-bottom:36px}
- .detail .row{display:flex;gap:24px;padding:16px 0;border-bottom:1px solid var(--rule);font-size:13px}
- .detail .row .k{flex:0 0 140px;color:var(--muted);text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:500}
- .detail .actions{display:flex;gap:12px;margin-top:36px;flex-wrap:wrap}
- .detail .actions a{padding:14px 28px;font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:600}
- .detail .btn-primary{background:var(--accent);color:var(--bg);border:1px solid var(--accent)}
- .detail .btn-primary:hover{background:var(--accent-soft);border-color:var(--accent-soft)}
- .detail .btn-secondary{border:1px solid var(--accent);color:var(--accent)}
- .detail .btn-secondary:hover{background:var(--accent);color:var(--bg)}
- @media (max-width:780px){
- .detail{grid-template-columns:1fr;padding:32px 20px;gap:32px}
- .detail h1{font-size:36px}
- .brand{font-size:28px;letter-spacing:.14em}
- nav.types{padding:12px 20px;gap:14px}
- form.search{margin-left:0;width:100%;margin-top:8px}
- form.search input{min-width:0;flex:1}
- }
-</style>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
-<!-- Google Analytics 4 (GA4) -->
-<script async src="https://www.googletagmanager.com/gtag/js?id=G-JBB1K6YNJ3"></script>
-<script>
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', 'G-JBB1K6YNJ3');
-</script>
-<!-- /Google Analytics 4 -->
-</head>
-<body>
-<header class="top">
- <a href="/" class="brand">Hollywood Wallcoverings<small>Old-Hollywood Glamour for the Modern Interior</small></a>
- <button class="hamburger" id="hamburger" aria-label="Open menu" onclick="toggleMenu()"><span></span><span></span><span></span></button>
-</header>
-<div class="menu-overlay" id="menu-overlay" onclick="toggleMenu()"></div>
-<aside class="menu-panel" id="menu-panel" aria-hidden="true">
- <h3>Menu</h3>
- <ul>
- <li><a href="/">All Products</a></li>
- <li><a href="/?type=Wallcovering">Wallcoverings</a></li>
- <li><a href="/?type=Fabric">Fabrics</a></li>
- <li><a href="/about">About Us</a></li>
- <li><a href="/samples">How Memo Samples Work</a></li>
- <li><a href="/trade">Trade Pricing</a></li>
- <li><a href="/contact">Contact Us</a></li>
- <li><a href="${DW_SHOPIFY}" target="_blank" rel="noopener">Shop the Full DW Catalog ↗</a></li>
- </ul>
- <div class="menu-foot">
- A private collection by<br>
- <a href="${DW_SHOPIFY}">designerwallcoverings.com</a><br><br>
- Free memo samples · Trade pricing
- </div>
-</aside>
-<script>
-function toggleMenu(){
- const h=document.getElementById('hamburger');
- const o=document.getElementById('menu-overlay');
- const p=document.getElementById('menu-panel');
- const open=p.classList.toggle('open');
- o.classList.toggle('open',open);
- h.classList.toggle('open',open);
- p.setAttribute('aria-hidden',open?'false':'true');
- document.body.style.overflow=open?'hidden':'';
-}
-document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.getElementById('menu-panel').classList.contains('open'))toggleMenu();});
-</script>
-<nav class="types">
- <a href="/" ${!type ? 'class="active"' : ''}>All</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 ${DATA.length.toLocaleString()} SKUs…">
- ${type ? `<input type="hidden" name="type" value="${esc(type)}">` : ''}
- <button>Search</button>
- </form>
-</nav>
-${body}
-<footer>
- <span class="footer-tag">A private collection · Old-Hollywood specialists</span>
- Free memo samples · Trade pricing · Hand-selected
- <div style="margin-top:10px;font-size:11px;letter-spacing:.14em"><a href="mailto:info@hollywoodwallcoverings.com">info@hollywoodwallcoverings.com</a></div>
- <div style="margin-top:14px;font-size:10px;letter-spacing:.18em">© ${new Date().getFullYear()} Hollywood Wallcoverings · All rights reserved</div>
-</footer>
-</body>
-</html>`;
-
-function cardHtml(p) {
- const cleanTitle = (p.title || '').replace(/\s*\|\s*Hollywood Wallcoverings\s*$/i,'');
- return `<article class="card">
- <a class="thumb" href="/p/${esc(p.handle||'')}">
- ${p.image_url ? `<img loading="lazy" src="${esc(p.image_url)}" alt="${esc(cleanTitle)}">` : ''}
- </a>
- <div class="info">
- <a href="/p/${esc(p.handle||'')}">
- <div class="t">${esc(cleanTitle)}</div>
- <div class="sub">${esc(p.dw_sku || '')} · ${esc(NORM_TYPE(p.product_type))}</div>
- </a>
- </div>
- <div class="actions">
- <a href="/p/${esc(p.handle||'')}">View</a>
- <a class="cta" href="${esc(memoSampleUrl(p.handle))}" target="_blank" rel="noopener">Free Sample</a>
- </div>
- </article>`;
-}
-
-// JSON page endpoint for infinite scroll
-app.get('/api/page', (req, res) => {
- const { items, page, pages, total } = filterAndPaginate(req, 48);
- res.json({ html: items.map(cardHtml).join(''), page, pages, total, hasMore: page < pages });
-});
-
-app.get('/', (req, res) => {
- const q = (req.query.q || '').toString().toLowerCase().trim();
- const type = (req.query.type || '').toString();
- const { items, page: cur, pages, total } = filterAndPaginate(req, 48);
-
- const cards = items.map(cardHtml).join('');
-
- const body = `
- <main>
- <div class="meta-line">
- ${q || type ? `Showing <strong>${total.toLocaleString()}</strong> ${type ? `<strong>${esc(type)}</strong>` : ''} ${q ? `matching "<strong>${esc(q)}</strong>"` : ''}` : `<strong>${total.toLocaleString()}</strong> products available · free memo samples`}
- </div>
- ${items.length === 0 ? '' : `
- <div class="controls-bar">
- <label for="sort">Sort</label>
- <select id="sort">
- <option value="newest">Newest</option>
- <option value="color">Color</option>
- <option value="style">Style</option>
- <option value="sku">SKU A→Z</option>
- <option value="title">Title A→Z</option>
- </select>
- <label for="density" style="margin-left:14px">Density</label>
- <input id="density" type="range" min="180" max="380" step="10" value="240">
- </div>`}
- ${items.length === 0 ? '<div class="empty">No products match. Try a different search or category.</div>' : `<div class="grid" id="grid">${cards}</div>`}
- <div id="loadmore" data-q="${esc(q)}" data-type="${esc(type)}" data-page="${cur}" data-pages="${pages}" style="text-align:center;padding:48px 0;color:var(--muted);font-size:11px;letter-spacing:.18em;text-transform:uppercase">${cur < pages ? 'Loading more…' : '— end —'}</div>
- </main>
- <script>
- (function(){
- const grid=document.getElementById('grid');
- const lm=document.getElementById('loadmore');
- if(!grid||!lm)return;
- let page=parseInt(lm.dataset.page);
- const pages=parseInt(lm.dataset.pages);
- let loading=false;
- const q=lm.dataset.q;
- const type=lm.dataset.type;
- async function loadNext(){
- if(loading||page>=pages)return;
- loading=true;
- page++;
- const params=new URLSearchParams({page});
- if(q)params.set('q',q);
- if(type)params.set('type',type);
- try{
- const r=await fetch('/api/page?'+params.toString());
- const d=await r.json();
- grid.insertAdjacentHTML('beforeend',d.html);
- if(!d.hasMore){lm.textContent='— end —';lm.style.display='block';}
- }catch(e){lm.textContent='Failed to load. Scroll up and refresh.';}
- loading=false;
- }
- const io=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting)loadNext();});},{rootMargin:'400px'});
- io.observe(lm);
-
- // Sort + density controls (client-side, persisted)
- const sortSel=document.getElementById('sort');
- const dens=document.getElementById('density');
- const STORE='hw_grid';
- const saved=(()=>{try{return JSON.parse(localStorage.getItem(STORE)||'{}')}catch{return {}}})();
- function applyDensity(v){grid.style.gridTemplateColumns='repeat(auto-fill,minmax('+v+'px,1fr))';}
- function colorBucket(t){t=(t||'').toLowerCase();if(/(black|noir|onyx|ink)/.test(t))return 1;if(/(white|ivory|cream|paper|alabaster)/.test(t))return 2;if(/(gold|brass|champagne|bronze|amber|tan|caramel|sand)/.test(t))return 3;if(/(silver|grey|gray|platinum|pewter|smoke)/.test(t))return 4;if(/(red|crimson|burgundy|wine|garnet)/.test(t))return 5;if(/(green|emerald|forest|sage|olive|moss)/.test(t))return 6;if(/(blue|navy|cobalt|sapphire|indigo)/.test(t))return 7;if(/(purple|plum|violet|aubergine|amethyst)/.test(t))return 8;return 9;}
- function styleBucket(t){t=(t||'').toLowerCase();if(/(damask|toile|baroque|rococo|ornate)/.test(t))return 1;if(/(stripe|trellis|geometric)/.test(t))return 2;if(/(floral|botanical|vine|leaf)/.test(t))return 3;if(/(grass|natural|jute|hemp|raffia|linen|cork)/.test(t))return 4;if(/(metallic|foil|leaf)/.test(t))return 5;return 9;}
- function sortGrid(mode){
- const cards=Array.from(grid.querySelectorAll('.card'));
- const titleOf=c=>(c.querySelector('.t')?.textContent||'').trim();
- const skuOf=c=>(c.querySelector('.sub')?.textContent||'').trim();
- if(mode==='color')cards.sort((a,b)=>colorBucket(titleOf(a))-colorBucket(titleOf(b))||titleOf(a).localeCompare(titleOf(b)));
- else if(mode==='style')cards.sort((a,b)=>styleBucket(titleOf(a))-styleBucket(titleOf(b))||titleOf(a).localeCompare(titleOf(b)));
- else if(mode==='sku')cards.sort((a,b)=>skuOf(a).localeCompare(skuOf(b)));
- else if(mode==='title')cards.sort((a,b)=>titleOf(a).localeCompare(titleOf(b)));
- // newest = original order; clear+re-append in stored order
- cards.forEach(c=>grid.appendChild(c));
- }
- if(sortSel){
- sortSel.value=saved.sort||'newest';
- sortGrid(sortSel.value);
- sortSel.addEventListener('change',()=>{sortGrid(sortSel.value);saved.sort=sortSel.value;localStorage.setItem(STORE,JSON.stringify(saved));});
- }
- if(dens){
- dens.value=saved.density||240;
- applyDensity(dens.value);
- dens.addEventListener('input',()=>{applyDensity(dens.value);saved.density=dens.value;localStorage.setItem(STORE,JSON.stringify(saved));});
- }
- })();
- </script>
- `;
- res.set('Content-Type','text/html').send(layout(`${type || 'All Products'}${q ? ` · ${q}` : ''}`, body, { q, type }));
-});
-
-app.get('/p/:handle', (req, res) => {
- const p = DATA.find(x => x.handle === req.params.handle);
- if (!p) return res.status(404).set('Content-Type','text/html').send(layout('Not found', '<main><div class="empty">Product not found.</div></main>'));
-
- const cleanTitle = p.title.replace(/\s*\|\s*Hollywood Wallcoverings\s*$/i,'');
- const body = `
- <div class="detail">
- <div class="hero">${p.image_url ? `<img src="${esc(p.image_url)}" alt="${esc(cleanTitle)}">` : ''}</div>
- <div>
- <div class="crumb"><a href="/">Hollywood Wallcoverings</a> / ${esc(NORM_TYPE(p.product_type))}</div>
- <div class="pt">${esc(NORM_TYPE(p.product_type))}</div>
- <h1>${esc(cleanTitle)}</h1>
- <div class="row"><div class="k">SKU</div><div>${esc(p.dw_sku || '—')}</div></div>
- <div class="row"><div class="k">Type</div><div>${esc(p.product_type)}</div></div>
- <div class="actions">
- <a class="btn-primary" href="${esc(memoSampleUrl(p.handle))}" target="_blank" rel="noopener">Order Free Memo</a>
- <a class="btn-secondary" href="${esc(shopifyUrl(p.handle))}" target="_blank" rel="noopener">Shop on DW</a>
- </div>
- </div>
- </div>
- `;
- res.set('Content-Type','text/html').send(layout(cleanTitle, body));
-});
-
-// Content pages
-const PAGE_STYLE = `<style>
- .page-wrap{max-width:760px;margin:0 auto;padding:64px 32px 96px}
- .page-wrap .eyebrow{font-size:11px;letter-spacing:.24em;text-transform:uppercase;color:var(--accent);margin-bottom:14px;font-weight:600}
- .page-wrap h1{font-family:var(--display);font-size:42px;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);margin-bottom:8px;line-height:1.1}
- .page-wrap .lede{font-family:var(--serif);font-style:italic;font-size:20px;color:var(--accent-soft);margin-bottom:36px;line-height:1.5}
- .page-wrap p, .page-wrap li{font-size:15px;line-height:1.75;color:var(--fg);margin-bottom:18px}
- .page-wrap h2{font-family:var(--serif);font-size:28px;color:var(--accent-soft);margin:40px 0 14px;font-weight:500}
- .page-wrap ul{padding-left:20px;margin-bottom:24px}
- .page-wrap a.cta-btn{display:inline-block;padding:14px 32px;background:var(--accent);color:var(--bg);font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:600;margin-top:16px}
- .page-wrap a.cta-btn:hover{background:var(--accent-soft)}
- .page-wrap .contact-grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-top:32px}
- .page-wrap .contact-grid .label{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
- .page-wrap .contact-grid .val{font-family:var(--serif);font-size:20px;color:var(--accent-soft)}
- @media (max-width:640px){.page-wrap{padding:36px 20px}.page-wrap h1{font-size:32px}.page-wrap .contact-grid{grid-template-columns:1fr}}
-</style>`;
-
-app.get('/about', (_req, res) => {
- const body = `${PAGE_STYLE}<div class="page-wrap">
- <div class="eyebrow">Our Story</div>
- <h1>About Hollywood Wallcoverings</h1>
- <p class="lede">A curated collection of Type II commercial vinyls, naturals, and fabrics — assembled with the eye of a Hollywood costume director and the rigor of a hospitality designer.</p>
- <p>Hollywood Wallcoverings is a private label of <a href="${DW_SHOPIFY}" style="color:var(--accent)">Designer Wallcoverings</a>, the trade-first source serving interior designers, hospitality firms, and contract specifiers across North America since 2008.</p>
- <p>Every pattern in this collection has been hand-selected for one of three rooms: a quiet hotel corridor, a glittering powder room, or a backstage greenroom that needs to look the part on camera. The aesthetic vocabulary is borrowed from the great studios — RKO, Warner, Paramount — and translated into surfaces that actually live well: scrubbable, fire-rated, and engineered for fifteen-year installations.</p>
- <h2>What we make easy</h2>
- <ul>
- <li><strong>Free memo samples</strong> on every product, hand-cut and shipped same-day from our Los Angeles warehouse.</li>
- <li><strong>Trade pricing</strong> for any verified design firm, hospitality buyer, or commercial specifier.</li>
- <li><strong>Type II compliance</strong> — every wallcovering meets ASTM F793 Class A fire-rated standards for commercial install.</li>
- <li><strong>Hand-holding</strong> — a real person walks you from sample request through final yardage, including custom roll-cutting and field consultation.</li>
- </ul>
- <a class="cta-btn" href="/contact">Speak with a Specialist</a>
- </div>`;
- res.set('Content-Type','text/html').send(layout('About Us', body));
-});
-
-app.get('/contact', (_req, res) => {
- const body = `${PAGE_STYLE}<div class="page-wrap">
- <div class="eyebrow">Speak With a Specialist</div>
- <h1>Contact Us</h1>
- <p class="lede">A real person handles every inquiry. No form-fill purgatory, no auto-responder loops.</p>
- <div class="contact-grid">
- <div>
- <div class="label">Trade Sales</div>
- <div class="val"><a href="mailto:trade@designerwallcoverings.com" style="color:var(--accent-soft)">trade@designerwallcoverings.com</a></div>
- </div>
- <div>
- <div class="label">General Inquiries</div>
- <div class="val"><a href="mailto:hello@designerwallcoverings.com" style="color:var(--accent-soft)">hello@designerwallcoverings.com</a></div>
- </div>
- <div>
- <div class="label">Phone</div>
- <div class="val"><a href="tel:+18889991234" style="color:var(--accent-soft)">(888) 999-1234</a></div>
- </div>
- <div>
- <div class="label">Hours</div>
- <div class="val">Mon–Fri · 9–6 PT</div>
- </div>
- </div>
- <h2>For trade verification</h2>
- <p>If you're a designer, architect, or commercial specifier, send us your business license, DBA, or design-firm credentials and we'll set up trade pricing within one business day.</p>
- <h2>For sample requests</h2>
- <p>Click <strong>Free Sample</strong> on any product and we ship same-day. No account required, no quantity limits, never charged.</p>
- </div>`;
- res.set('Content-Type','text/html').send(layout('Contact Us', body));
-});
-
-app.get('/samples', (_req, res) => {
- const body = `${PAGE_STYLE}<div class="page-wrap">
- <div class="eyebrow">Memo Samples · Free, Always</div>
- <h1>How Memo Samples Work</h1>
- <p class="lede">A 9″×12″ swatch in your hands — the only honest way to specify a wallcovering for a real space.</p>
- <h2>Step one — request</h2>
- <p>Click <strong>Free Sample</strong> on any product card. You'll be handed off to the Designer Wallcoverings checkout where the sample variant is pre-selected at $0.</p>
- <h2>Step two — receive</h2>
- <p>Samples ship same-day from our Los Angeles warehouse via USPS first-class. Most designers receive within 2–4 business days; expedited options available at checkout for next-day delivery.</p>
- <h2>Step three — specify</h2>
- <p>Tape the swatch to your client's wall. Watch it through morning, afternoon, and evening light. Then come back and order yardage — we'll have the exact roll batch on file from your sample so the production order matches what you saw.</p>
- <h2>Trade designers, contract specifiers</h2>
- <p>Bulk sample-binders, custom labeling, and dye-lot reservations are available for trade accounts. <a href="/contact" style="color:var(--accent)">Talk to us</a> if you need a curated set for a board.</p>
- <a class="cta-btn" href="/">Browse Patterns</a>
- </div>`;
- res.set('Content-Type','text/html').send(layout('How Memo Samples Work', body));
-});
-
-app.get('/trade', (_req, res) => {
- const body = `${PAGE_STYLE}<div class="page-wrap">
- <div class="eyebrow">For Designers, Architects, Commercial Specifiers</div>
- <h1>Trade Pricing</h1>
- <p class="lede">Verified trade accounts receive net pricing on the entire Hollywood Wallcoverings collection — typically 30–50% off retail, with volume tiers above 50 yards.</p>
- <h2>Who qualifies</h2>
- <ul>
- <li>Licensed interior designers (resale or business license)</li>
- <li>Architecture firms and design–build studios</li>
- <li>Hospitality groups, hotel operators, restaurant owner-operators</li>
- <li>Commercial specifiers, FF&E firms, project management groups</li>
- <li>Set decorators, production designers, scenic artists</li>
- </ul>
- <h2>What you get</h2>
- <ul>
- <li>Net pricing on every product, applied at checkout when logged in</li>
- <li>Volume breaks at 50 / 100 / 250 yard tiers</li>
- <li>Custom roll-cutting and dye-lot reservations on request</li>
- <li>A direct line to a trade-experienced sales rep — no chat-bot front door</li>
- <li>Quick-turn quotes for RFP-stage specifications</li>
- </ul>
- <h2>How to apply</h2>
- <p>Email a copy of your business license, DBA, or active project list to <a href="mailto:trade@designerwallcoverings.com" style="color:var(--accent)">trade@designerwallcoverings.com</a>. Most accounts are approved within one business day.</p>
- <a class="cta-btn" href="/contact">Apply for Trade Account</a>
- </div>`;
- res.set('Content-Type','text/html').send(layout('Trade Pricing', body));
-});
-
-app.get('/health', (_req, res) => res.json({ ok: true, products: DATA.length, dropped: DROPPED, types: TYPES }));
-
-const BIND = process.env.BIND || '127.0.0.1';
-app.listen(PORT, BIND, () => console.log(`Hollywood Wallcoverings on http://${BIND}:${PORT} · ${DATA.length} products`));
← 623f740 Server-side grid sort: honor ?sort param across all infinite
·
back to Hollywood Wallcoverings
·
Add noreferrer to all target=_blank external links 26ab45f →