[object Object]

← back to Interiordesignershowroom

hardening: branded 404 page + remove orphan public/js/cart.js

cb035b818b286fc9326f11d53f0b9d58fe31411e · 2026-08-09 09:08:48 -0700 · Steve Abrams

- 404 handler was a bare 'Not found / back to showroom' line; now a branded
  Error-404 page with helpful destinations (Shop/Rooms/Build/Guides), inline
  styles matching the footer pattern (no CSP/style-src impact), reusing layout
  header+footer. Verified: /nope -> 404 status + new copy, home still 200.
- public/js/cart.js was dead scaffolding from the initial generic-ecommerce
  base — loaded by no script (this is an affiliate showroom; click-through is
  /go, there is no cart/checkout). Removed; moodboard.js comment de-referenced.
- /shop no-results state already handled (server.js grid-empty); sitemap
  lastmod left as-is (guides/rooms already carry accurate lastmod; fabricating
  static-page dates would be an SEO-negative, not an improvement).

Deploy to Kamatera is GATED -> drafted to pending-approval.

Files touched

Diff

commit cb035b818b286fc9326f11d53f0b9d58fe31411e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 9 09:08:48 2026 -0700

    hardening: branded 404 page + remove orphan public/js/cart.js
    
    - 404 handler was a bare 'Not found / back to showroom' line; now a branded
      Error-404 page with helpful destinations (Shop/Rooms/Build/Guides), inline
      styles matching the footer pattern (no CSP/style-src impact), reusing layout
      header+footer. Verified: /nope -> 404 status + new copy, home still 200.
    - public/js/cart.js was dead scaffolding from the initial generic-ecommerce
      base — loaded by no script (this is an affiliate showroom; click-through is
      /go, there is no cart/checkout). Removed; moodboard.js comment de-referenced.
    - /shop no-results state already handled (server.js grid-empty); sitemap
      lastmod left as-is (guides/rooms already carry accurate lastmod; fabricating
      static-page dates would be an SEO-negative, not an improvement).
    
    Deploy to Kamatera is GATED -> drafted to pending-approval.
---
 public/js/cart.js      | 140 -------------------------------------------------
 public/js/moodboard.js |   2 +-
 server.js              |  16 +++++-
 3 files changed, 16 insertions(+), 142 deletions(-)

diff --git a/public/js/cart.js b/public/js/cart.js
deleted file mode 100644
index 564515a..0000000
--- a/public/js/cart.js
+++ /dev/null
@@ -1,140 +0,0 @@
-// Affiliate "shopping list" cart — the BEHAVIOR layer for the server-rendered
-// `.add-cart` buttons (see productCard/addCartBtn in lib/render.js). Because every
-// product checks out at a DIFFERENT merchant via its own /go/:id tracked link, there
-// is no unified checkout — the shopper collects items and Checkout opens EACH item
-// at its store in its own tab (one high-intent burst of tracked links). Pure
-// client-side: state in localStorage, UI + styles injected here, buttons wired by
-// delegation so infinite-scroll / builder-added cards work too.
-(function () {
-  var KEY = 'ids_cart';
-  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
-  function load() { try { return JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { return []; } }
-  function save(c) { localStorage.setItem(KEY, JSON.stringify(c)); sync(); }
-  function has(id) { return load().some(function (x) { return String(x.id) === String(id); }); }
-  function addItem(it) { var c = load(); if (!c.some(function (x) { return String(x.id) === String(it.id); })) { c.push(it); save(c); } }
-  function removeItem(id) { save(load().filter(function (x) { return String(x.id) !== String(id); })); }
-
-  // ---------- injected styles (fab + drawer + the .add-cart/.card-actions buttons) ----------
-  var css = ''
-    + '.card-actions{display:flex;flex-direction:column;gap:6px;margin-top:10px}'
-    + '.add-cart{border:1px solid var(--line,#e8e2d9);background:var(--bg,#faf8f5);border-radius:2px;padding:6px 8px;'
-    + 'font:.68rem/1 var(--sans,sans-serif);letter-spacing:.06em;text-transform:uppercase;cursor:pointer;color:#3a3530;'
-    + 'transition:border-color .18s,color .18s,background .18s}'
-    + '.add-cart:hover{border-color:#9b7b50;color:#9b7b50}'
-    + '.add-cart[aria-pressed="true"]{border-color:#9b7b50;color:#9b7b50;background:rgba(155,123,80,.08)}'
-    + '.cartfab{position:fixed;right:20px;bottom:20px;z-index:300;display:flex;align-items:center;gap:7px;'
-    + 'background:#16130f;color:#faf8f5;border:0;border-radius:30px;padding:11px 17px;font:500 .8rem/1 var(--sans,sans-serif);'
-    + 'letter-spacing:.04em;cursor:pointer;box-shadow:0 8px 26px rgba(22,19,15,.28);transition:transform .15s,background .2s}'
-    + '.cartfab:hover{transform:translateY(-2px);background:#9b7b50}'
-    + '.cartfab .cartfab-n{display:inline-grid;place-items:center;min-width:19px;height:19px;padding:0 5px;border-radius:11px;'
-    + 'background:#c9a96e;color:#16130f;font-size:.72rem;font-weight:600}'
-    + '.cartfab:not(.has){opacity:.82}'
-    + '.cart-backdrop{position:fixed;inset:0;z-index:340;background:rgba(22,19,15,.42);backdrop-filter:blur(2px)}'
-    + '.cartdrawer{position:fixed;top:0;right:0;height:100vh;width:360px;max-width:90vw;z-index:350;background:#fff;'
-    + 'border-left:1px solid var(--line,#e8e2d9);box-shadow:-10px 0 40px rgba(22,19,15,.18);display:flex;flex-direction:column;'
-    + 'font-family:var(--sans,sans-serif);animation:cartslide .26s cubic-bezier(.22,1,.36,1)}'
-    + '@keyframes cartslide{from{transform:translateX(20px);opacity:.6}to{transform:none;opacity:1}}'
-    + '.cartdrawer-head{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid var(--line,#e8e2d9);'
-    + 'font-family:var(--serif,serif);font-size:1.15rem;color:#16130f}'
-    + '.cartdrawer-x{border:0;background:none;font-size:1.1rem;color:#7a7168;cursor:pointer;line-height:1}'
-    + '.cart-items{flex:1;overflow-y:auto;padding:8px 14px}'
-    + '.cart-empty{color:#7a7168;font-style:italic;text-align:center;padding:40px 20px;line-height:1.6;font-size:.86rem}'
-    + '.cart-item{display:flex;gap:11px;align-items:flex-start;padding:10px 2px;border-bottom:1px solid var(--line-lt,#f1ece5)}'
-    + '.cart-item>img{width:52px;height:52px;object-fit:cover;border-radius:4px;border:1px solid var(--line,#e8e2d9);flex:0 0 52px;background:#f4f0e9}'
-    + '.cart-item-b{flex:1;min-width:0}'
-    + '.cart-item-t{font-size:.78rem;color:#3a3530;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}'
-    + '.cart-item-m{font-size:.72rem;color:#7a7168;margin:2px 0 4px}'
-    + '.cart-item-open{font-size:.7rem;color:#7a6038;text-transform:uppercase;letter-spacing:.05em}'
-    + '.cart-item-open:hover{text-decoration:underline}'
-    + '.cart-item-rm{border:0;background:none;color:#b0a898;cursor:pointer;font-size:.9rem;padding:2px}'
-    + '.cart-item-rm:hover{color:#8b3a3a}'
-    + '.cart-foot{border-top:1px solid var(--line,#e8e2d9);padding:14px 16px;display:flex;flex-direction:column;gap:8px}'
-    + '.cart-checkout{background:#16130f;color:#faf8f5;border:0;border-radius:2px;padding:13px;font:500 .8rem/1 var(--sans,sans-serif);'
-    + 'letter-spacing:.08em;text-transform:uppercase;cursor:pointer;transition:background .2s}'
-    + '.cart-checkout:hover{background:#9b7b50}'
-    + '.cart-clear{background:none;border:0;color:#7a7168;font-size:.72rem;cursor:pointer;letter-spacing:.03em}'
-    + '.cart-clear:hover{color:#8b3a3a}'
-    + '.cart-note{margin:2px 0 0;font-size:.66rem;color:#b0a898;line-height:1.5}'
-    + '.cartdrawer[hidden],.cart-backdrop[hidden]{display:none}'   // explicit display beats [hidden] UA rule
-    + '@media(max-width:520px){.cartfab{right:14px;bottom:14px}}';
-  var st = document.createElement('style'); st.textContent = css; document.head.appendChild(st);
-
-  // ---------- floating button + drawer ----------
-  var fab = document.createElement('button');
-  fab.className = 'cartfab'; fab.type = 'button'; fab.setAttribute('aria-label', 'Open your shopping list');
-  fab.innerHTML = '🛍 <span class="cartfab-n">0</span>';
-  var backdrop = document.createElement('div'); backdrop.className = 'cart-backdrop'; backdrop.hidden = true;
-  var drawer = document.createElement('div'); drawer.className = 'cartdrawer'; drawer.hidden = true;
-  drawer.setAttribute('role', 'dialog'); drawer.setAttribute('aria-label', 'Your shopping list');
-  drawer.innerHTML = '<div class="cartdrawer-head">Your list <button class="cartdrawer-x" type="button" aria-label="Close">✕</button></div>'
-    + '<div class="cart-items"></div><div class="cart-foot"></div>';
-  document.body.appendChild(fab); document.body.appendChild(backdrop); document.body.appendChild(drawer);
-
-  function renderDrawer() {
-    var c = load(), items = drawer.querySelector('.cart-items'), foot = drawer.querySelector('.cart-foot');
-    if (!c.length) { items.innerHTML = '<p class="cart-empty">Your list is empty.<br>Add pieces you love, then open them all to shop at each store.</p>'; foot.innerHTML = ''; return; }
-    items.innerHTML = c.map(function (x) {
-      return '<div class="cart-item"><img src="' + esc(x.image) + '" alt="" data-hide-on-error="1">'
-        + '<div class="cart-item-b"><div class="cart-item-t">' + esc(x.title) + '</div>'
-        + '<div class="cart-item-m">' + esc(x.price || '') + (x.advertiser ? ' · ' + esc(x.advertiser) : '') + '</div>'
-        + '<a class="cart-item-open" href="' + esc(x.url) + '" target="_blank" rel="nofollow sponsored">Open →</a></div>'
-        + '<button class="cart-item-rm" type="button" data-rm="' + esc(x.id) + '" aria-label="Remove">✕</button></div>';
-    }).join('');
-    foot.innerHTML = '<button class="cart-checkout" type="button">Checkout — open all ' + c.length + ' tab' + (c.length === 1 ? '' : 's') + '</button>'
-      + '<button class="cart-clear" type="button">Clear list</button>'
-      + '<p class="cart-note">Checkout opens each item at its store in a new tab. If your browser blocks them, allow pop-ups for this site — or tap “Open →” on each.</p>';
-  }
-  function openDrawer() { renderDrawer(); backdrop.hidden = false; drawer.hidden = false; document.body.style.overflow = 'hidden'; }
-  function closeDrawer() { drawer.hidden = true; backdrop.hidden = true; document.body.style.overflow = ''; }
-  function checkout() {
-    var c = load(); if (!c.length) return;
-    var blocked = 0;
-    c.forEach(function (x) { var w = window.open(x.url, '_blank', 'noopener'); if (!w) blocked++; });
-    if (blocked) setTimeout(function () { alert(blocked + ' of ' + c.length + ' tab' + (blocked === 1 ? ' was' : 's were') + ' blocked by your browser. Allow pop-ups for this site, or use the “Open →” link on each item.'); }, 60);
-  }
-
-  // read a cart item off a server-rendered .add-cart button (+ its card for advertiser)
-  function itemFromBtn(btn) {
-    var id = btn.getAttribute('data-id'); if (!id) return null;
-    var card = btn.closest('.card'), brand = card && card.querySelector('.card-brand');
-    var adv = (card && (card.getAttribute('data-network') || (brand ? brand.textContent : ''))) || '';
-    return {
-      id: id, title: btn.getAttribute('data-title') || '', price: btn.getAttribute('data-price') || '',
-      image: btn.getAttribute('data-img') || '', advertiser: adv.trim(),
-      url: btn.getAttribute('data-go') || ('/go/' + id),
-    };
-  }
-  function syncBtn(btn) {
-    var on = has(btn.getAttribute('data-id'));
-    btn.setAttribute('aria-pressed', on ? 'true' : 'false');
-    var l = btn.querySelector('.add-cart-label') || btn;
-    l.textContent = on ? '✓ On list' : 'Add to cart';
-  }
-  function sync() {
-    fab.querySelector('.cartfab-n').textContent = load().length;
-    fab.classList.toggle('has', load().length > 0);
-    document.querySelectorAll('.add-cart').forEach(syncBtn);
-    if (!drawer.hidden) renderDrawer();
-  }
-
-  // ---------- events ----------
-  fab.addEventListener('click', openDrawer);
-  backdrop.addEventListener('click', closeDrawer);
-  document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !drawer.hidden) closeDrawer(); });
-  drawer.addEventListener('click', function (e) {
-    if (e.target.closest('.cartdrawer-x')) return closeDrawer();
-    if (e.target.closest('.cart-checkout')) return checkout();
-    if (e.target.closest('.cart-clear')) { save([]); return; }
-    var rm = e.target.closest('[data-rm]'); if (rm) removeItem(rm.getAttribute('data-rm'));
-  });
-  document.addEventListener('click', function (e) {
-    var b = e.target.closest('.add-cart'); if (!b) return;
-    e.preventDefault();
-    var it = itemFromBtn(b); if (!it) return;
-    if (has(it.id)) removeItem(it.id); else addItem(it);
-  });
-
-  // dynamically-added cards (infinite scroll, builder) → keep button states in sync
-  var t; new MutationObserver(function () { clearTimeout(t); t = setTimeout(sync, 150); }).observe(document.body, { childList: true, subtree: true });
-  sync();
-})();
diff --git a/public/js/moodboard.js b/public/js/moodboard.js
index 63a6def..dcfeeba 100644
--- a/public/js/moodboard.js
+++ b/public/js/moodboard.js
@@ -1,7 +1,7 @@
 // Mood Board — collect pieces while shopping, then turn the whole board into ONE
 // photoreal room setting on a button push. Distinct from the buy-cart: the cart is
 // "things to purchase, each at its own store"; the mood board is "pieces I'm composing
-// into a room". Pure client-side behavior layer (mirrors cart.js): state in
+// into a room". Pure client-side behavior layer: state in
 // localStorage, styles + FAB + drawer injected here, per-card buttons wired by
 // delegation so infinite-scroll-added cards work too. The heavy lifting (compose the
 // selected products into an Architectural-Digest-grade room) is done server-side by the
diff --git a/server.js b/server.js
index fa3ecaa..d30ed86 100644
--- a/server.js
+++ b/server.js
@@ -700,7 +700,21 @@ app.get('/room/:slug', async (req, res, next) => {
 
 app.use(require('./routes/admin'));
 
-app.use((_req, res) => res.status(404).send(layout({ title: 'Not found', body: '<section><h1>Not found</h1><p><a href="/">Back to the showroom →</a></p></section>' })));
+app.use((_req, res) => res.status(404).send(layout({
+  title: 'Page not found',
+  description: 'That page moved or never existed — browse the showroom instead.',
+  body: `<section style="text-align:center;padding:64px 20px;max-width:640px;margin:0 auto">
+  <p class="subtle" style="letter-spacing:.18em;text-transform:uppercase;font-size:.78rem;margin:0">Error 404</p>
+  <h1 style="font-family:'DM Serif Display',serif;font-size:2.4rem;line-height:1.15;margin:.25em 0">This page has moved on</h1>
+  <p class="subtle" style="margin:0 auto 30px;max-width:44ch">We couldn't find what you were looking for — but there's plenty more to discover in the showroom.</p>
+  <div style="display:flex;flex-wrap:wrap;gap:12px;justify-content:center">
+    <a href="/shop" style="display:inline-block;padding:12px 22px;border:1px solid currentColor;border-radius:999px;text-decoration:none">Shop everything</a>
+    <a href="/looks" style="display:inline-block;padding:12px 22px;border:1px solid currentColor;border-radius:999px;text-decoration:none">Browse rooms</a>
+    <a href="/build" style="display:inline-block;padding:12px 22px;border:1px solid currentColor;border-radius:999px;text-decoration:none">Build a room</a>
+    <a href="/guides" style="display:inline-block;padding:12px 22px;border:1px solid currentColor;border-radius:999px;text-decoration:none">Read the guides</a>
+  </div>
+</section>`,
+})));
 app.use((err, _req, res, _next) => { console.error(err); res.status(500).send(layout({ title: 'Error', body: '<section><h1>Something went wrong</h1></section>' })); });
 
 // Bind to loopback by default (matches the log + keeps the app behind the proxy rather

← e5b86aa Remove AdSense loader + ads.txt (luxury/commerce surface, no  ·  back to Interiordesignershowroom  ·  fix(csp): allow static.cloudflareinsights.com in public scri 23f8ed4 →