[object Object]

← back to Thedesignerlibrary

agent-review refinements: GPU disposal + tap-to-flip + XSS escaping + SEO surface (robots/sitemap/OG/JSON-LD/SSR browse) + perf caches + luxury type scale

5808dabd221fcfb63205fa6054241b3f6216b676 · 2026-08-07 08:15:10 -0700 · Steve Abrams

Files touched

Diff

commit 5808dabd221fcfb63205fa6054241b3f6216b676
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 08:15:10 2026 -0700

    agent-review refinements: GPU disposal + tap-to-flip + XSS escaping + SEO surface (robots/sitemap/OG/JSON-LD/SSR browse) + perf caches + luxury type scale
---
 public/bookshelf.js                                |  71 +++++++--
 public/browse.html                                 | 105 +++++++++-----
 public/favicon.svg                                 |   8 +
 public/index.html                                  |  97 +++++++++++--
 public/og-image.jpg                                | Bin 0 -> 51797 bytes
 .../{three.module.js => three-0.160.0.module.js}   |   0
 server.js                                          | 161 +++++++++++++++------
 7 files changed, 343 insertions(+), 99 deletions(-)

diff --git a/public/bookshelf.js b/public/bookshelf.js
index 3b02d44..c440f55 100644
--- a/public/bookshelf.js
+++ b/public/bookshelf.js
@@ -49,6 +49,7 @@ export class BookshelfCatalog {
       accent: '#c9a227',
       autoRotateHint: true,       // gentle idle drift on the shelf
       onOpen: null,               // (book, index) => void
+      onClose: null,              // (book) => void ; fires when the book is reshelved
       onPageChange: null,         // (book, pageIndex) => void
       onLinkClick: null,          // (page, book) => void ; return false to prevent navigation
     }, opts);
@@ -154,19 +155,30 @@ export class BookshelfCatalog {
     return new THREE.Color().setHSL(hue / 360, 0.42, 0.42).getStyle();
   }
 
+  // spine/cover text ink chosen by luminance — white titles vanish on the
+  // White/Cream and Greige hue-books, so light spines get dark walnut ink
+  _inkFor(spineColor) {
+    const { r, g, b } = spineColor;
+    const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
+    return lum > 0.6
+      ? { main: '#2b2118', dim: 'rgba(43,33,24,0.75)', band: 'rgba(43,33,24,0.18)' }
+      : { main: '#fff', dim: 'rgba(255,255,255,0.82)', band: 'rgba(255,255,255,0.10)' };
+  }
+
   _spineTexture(book, spineColor) {
     const cw = 256, ch = 1024;
     const c = document.createElement('canvas'); c.width = cw; c.height = ch;
     const g = c.getContext('2d');
+    const ink = this._inkFor(spineColor);
     g.fillStyle = spineColor.getStyle(); g.fillRect(0, 0, cw, ch);
     // subtle top/bottom bands
-    g.fillStyle = 'rgba(255,255,255,0.10)'; g.fillRect(0, 40, cw, 6); g.fillRect(0, ch - 46, cw, 6);
+    g.fillStyle = ink.band; g.fillRect(0, 40, cw, 6); g.fillRect(0, ch - 46, cw, 6);
     // vertical title
     g.save(); g.translate(cw / 2, ch / 2); g.rotate(-Math.PI / 2);
-    g.fillStyle = '#fff'; g.textAlign = 'center'; g.textBaseline = 'middle';
+    g.fillStyle = ink.main; g.textAlign = 'center'; g.textBaseline = 'middle';
     g.font = '600 62px Georgia, serif';
     g.fillText(this._fit(g, book.title || 'Untitled', ch - 120), 0, -18);
-    if (book.subtitle) { g.font = '400 34px Georgia, serif'; g.fillStyle = 'rgba(255,255,255,0.82)'; g.fillText(this._fit(g, book.subtitle, ch - 160), 0, 46); }
+    if (book.subtitle) { g.font = '400 34px Georgia, serif'; g.fillStyle = ink.dim; g.fillText(this._fit(g, book.subtitle, ch - 160), 0, 46); }
     g.restore();
     const tex = new THREE.CanvasTexture(c); tex.anisotropy = 4; return tex;
   }
@@ -179,11 +191,13 @@ export class BookshelfCatalog {
     grad.addColorStop(0, spineColor.clone().offsetHSL(0, 0, 0.08).getStyle());
     grad.addColorStop(1, spineColor.clone().offsetHSL(0, 0, -0.10).getStyle());
     g.fillStyle = grad; g.fillRect(0, 0, cw, ch);
-    g.strokeStyle = 'rgba(255,255,255,0.35)'; g.lineWidth = 3; g.strokeRect(24, 24, cw - 48, ch - 48);
-    g.fillStyle = '#fff'; g.textAlign = 'center';
+    const ink = this._inkFor(spineColor);
+    g.strokeStyle = ink.main === '#fff' ? 'rgba(255,255,255,0.35)' : 'rgba(43,33,24,0.35)';
+    g.lineWidth = 3; g.strokeRect(24, 24, cw - 48, ch - 48);
+    g.fillStyle = ink.main; g.textAlign = 'center';
     g.font = '700 52px Georgia, serif';
     this._wrap(g, book.title || 'Untitled', cw / 2, 220, cw - 100, 60);
-    if (book.subtitle) { g.font = '400 30px Georgia, serif'; g.fillStyle = 'rgba(255,255,255,0.85)'; g.fillText(this._fit(g, book.subtitle, cw - 100), cw / 2, ch - 90); }
+    if (book.subtitle) { g.font = '400 30px Georgia, serif'; g.fillStyle = ink.dim; g.fillText(this._fit(g, book.subtitle, cw - 100), cw / 2, ch - 90); }
     const tex = new THREE.CanvasTexture(c); tex.anisotropy = 4; return tex;
   }
 
@@ -204,10 +218,13 @@ export class BookshelfCatalog {
     g.fillText(`${pageNo} / ${count}`, cw - 44, ch - 34);
     if (page.href) { g.fillStyle = this.opts.accent; g.textAlign = 'left'; g.font = '600 22px Georgia, serif'; g.fillText('View  ›', 44, ch - 40); }
     const tex = new THREE.CanvasTexture(c); tex.anisotropy = 4;
-    // async swap in the real image on top of the placeholder, if provided
+    // async swap in the real image on top of the placeholder, if provided.
+    // Guard on _destroyed: the load can outlive a shelf-tab switch, and writing
+    // to a disposed texture is a WebGL error waiting to happen.
     if (page.image) {
       const img = new Image(); img.crossOrigin = 'anonymous';
       img.onload = () => {
+        if (this._destroyed) return;
         const r = Math.min((cw - 80) / img.width, 420 / img.height);
         const dw = img.width * r, dh = img.height * r;
         g.fillStyle = '#e8e3d6'; g.fillRect(40, 40, cw - 80, 420);
@@ -287,6 +304,9 @@ export class BookshelfCatalog {
       dragging = false;
       if (moved < 5) this._handleClick(e);   // treat as a click, not a drag
     });
+    // browser-cancelled pointer (two-finger scroll etc.) must clear the drag
+    // state or the next tap gets misread as a drag
+    el.addEventListener('pointercancel', () => { dragging = false; });
 
     // keyboard: arrows page/scroll, Esc closes (stored so destroy() can unbind —
     // this instance is rebuilt on every shelf-tab switch)
@@ -316,11 +336,16 @@ export class BookshelfCatalog {
       const hits = this.raycaster.intersectObjects(this._bookGroups.map(g => g.userData.body), false);
       if (hits.length) { this.openBook(hits[0].object.userData.index); return; }
     } else if (this.state === 'reading') {
-      // click right half -> next, left half -> prev, backdrop -> close
+      // right page: tap flips forward; ONLY the "View ›" strip (bottom-left of
+      // the page texture) opens the store link — otherwise a touch user can
+      // never see page 2 because every page has an href. left page: back.
+      // backdrop: close.
       const hits = this.raycaster.intersectObjects([this._leftPage, this._rightPage], false);
       if (hits.length) {
         const page = this.books[this.activeIndex].pages?.[this.pageIndex];
-        if (hits[0].object === this._rightPage && page && page.href) {
+        const uv = hits[0].uv;
+        const onViewStrip = uv && uv.y < 0.14 && uv.x < 0.5;
+        if (hits[0].object === this._rightPage && page && page.href && onViewStrip) {
           const ok = this.opts.onLinkClick ? this.opts.onLinkClick(page, this.books[this.activeIndex]) : true;
           if (ok !== false) window.open(page.href, '_blank', 'noopener,noreferrer');
           return;
@@ -339,6 +364,7 @@ export class BookshelfCatalog {
     const g = this._bookGroups[index];
     g.userData.body.visible = false;                 // hide the shelved spine
     const open = this._ensureOpenBook(); open.visible = true;
+    open.scale.setScalar(this._openBookScale());
     this._paintSpread(this.books[index]);
     // animate: book flies from shelf to reading position + opens flat
     open.position.set(g.position.x - this._scrollXCurrent, 0.4, 0.5);
@@ -352,10 +378,21 @@ export class BookshelfCatalog {
     if (this.opts.onOpen) this.opts.onOpen(this.books[index], index);
   }
 
+  // scale the open spread down on narrow (portrait) viewports so both pages
+  // stay inside the frame — on a phone the full-size spread crops both edges
+  _openBookScale() {
+    const dist = 11 - 6.2;                                        // camera z - reading z
+    const visH = 2 * dist * Math.tan((this.camera.fov / 2) * Math.PI / 180);
+    const visW = visH * this.camera.aspect;
+    const spreadW = BOOK_W * 2 + 0.1;
+    return clamp((visW * 0.94) / spreadW, 0.3, 1);
+  }
+
   closeBook() {
     if (this.state !== 'reading') return;
     this.state = 'closing';
     const open = this._openBook; const g = this._bookGroups[this.activeIndex];
+    const closedBook = this.books[this.activeIndex];
     this._tween(this._leftPage.rotation, 'y', 0, 0.9, 400, easeInOutCubic);
     this._tween(this._rightPage.rotation, 'y', 0, -0.9, 400, easeInOutCubic);
     this._tween(open.position, 'z', open.position.z, 0.5, 520, easeInOutCubic);
@@ -363,6 +400,7 @@ export class BookshelfCatalog {
     this._tween(open.rotation, 'y', 0, -0.9, 520, easeInOutCubic, () => {
       open.visible = false; g.userData.body.visible = true;
       this.state = 'shelf'; this.activeIndex = -1;
+      if (this.opts.onClose) this.opts.onClose(closedBook);
     });
   }
 
@@ -383,8 +421,7 @@ export class BookshelfCatalog {
     const from = page.rotation.y, mid = dir > 0 ? -Math.PI * 0.5 : Math.PI * 0.5;
     this._tween(page.rotation, 'y', from, mid, 160, easeInOutCubic, () => {
       this._paintSpread(this.books[this.activeIndex]);
-      this._tween(page.rotation, 'y', mid, dir > 0 ? 0 : 0, 160, easeInOutCubic);
-      page.rotation.y = 0;
+      this._tween(page.rotation, 'y', mid, 0, 160, easeInOutCubic);
     });
   }
 
@@ -426,12 +463,24 @@ export class BookshelfCatalog {
     const w = this.container.clientWidth || 1, h = this.container.clientHeight || 1;
     this.camera.aspect = w / h; this.camera.updateProjectionMatrix();
     this.renderer.setSize(w, h);
+    if (this._openBook && this._openBook.visible) this._openBook.scale.setScalar(this._openBookScale());
   }
 
   destroy() {
+    this._destroyed = true;
     cancelAnimationFrame(this._raf);
     window.removeEventListener('resize', this._onResize);
     window.removeEventListener('keydown', this._onKeydown);
+    // renderer.dispose() alone leaves every geometry/material/CanvasTexture on
+    // the GPU — this instance is rebuilt per shelf-tab switch, so without the
+    // traversal 4-5 switches visibly stutter a mobile GPU
+    this.scene.traverse((obj) => {
+      if (obj.geometry) obj.geometry.dispose();
+      if (obj.material) {
+        const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
+        for (const m of mats) { if (m.map) m.map.dispose(); m.dispose(); }
+      }
+    });
     this.renderer.dispose();
     if (this.renderer.domElement.parentNode) this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
   }
diff --git a/public/browse.html b/public/browse.html
index d2e736d..03dd7b9 100644
--- a/public/browse.html
+++ b/public/browse.html
@@ -4,25 +4,45 @@
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Browse the Catalog — The Designer Library</title>
-<meta name="description" content="Browse 84,000+ designer wallcoverings by brand, collection, style and hue.">
+<meta name="description" content="Browse 84,000+ designer wallcoverings from Phillipe Romano, Malibu Wallpaper, Rebel Walls, Clarke & Clarke, Thibaut and 40+ more brands — filtered by hue, style, collection or keyword. The full Designer Wallcoverings catalog, organized.">
+<link rel="canonical" href="https://thedesignerlibrary.com/browse">
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://thedesignerlibrary.com/browse">
+<meta property="og:title" content="Browse the Catalog — The Designer Library">
+<meta property="og:description" content="Browse 84,000+ designer wallcoverings by brand, collection, style and hue.">
+<meta property="og:image" content="https://thedesignerlibrary.com/og-image.jpg">
+<meta property="og:site_name" content="The Designer Library">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="Browse the Catalog — The Designer Library">
+<meta name="twitter:description" content="Browse 84,000+ designer wallcoverings by brand, collection, style and hue.">
+<meta name="twitter:image" content="https://thedesignerlibrary.com/og-image.jpg">
 <style>
   :root { --bg:#171310; --ink:#efe8db; --dim:#a99e8d; --brass:#c9a227; --cols:5; }
   * { box-sizing:border-box; }
-  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 Georgia, serif; }
+  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 Georgia, serif;
+    padding-bottom:90px; /* keep the last row + status clear of the DW-network footer-strip ad */ }
   header { display:flex; align-items:center; justify-content:space-between; padding:16px 22px; }
-  .wordmark { text-decoration:none; color:var(--ink); letter-spacing:.16em; font-size:18px; font-weight:600; text-transform:uppercase; }
+  .wordmark { text-decoration:none; color:var(--ink); letter-spacing:.14em;
+    font-size:clamp(26px, 2.8vw, 42px); font-weight:600; text-transform:uppercase; white-space:nowrap; }
   .wordmark em { color:var(--brass); font-style:normal; }
+  .tagline { font-style:italic; color:var(--dim); font-size:13px; margin-left:14px; letter-spacing:.04em; }
   .controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:0 22px 14px; }
   select,input[type=search] { background:#211b16; color:var(--ink); border:1px solid rgba(201,162,39,.3);
     border-radius:8px; padding:7px 10px; font:13px Georgia, serif; }
-  .density { display:flex; align-items:center; gap:6px; color:var(--dim); font-size:12px; }
+  .density { display:flex; align-items:center; gap:6px; color:var(--dim); font-size:12px;
+    border:1px solid rgba(201,162,39,.3); border-radius:8px; padding:5px 10px; }
+  #density { accent-color:var(--brass); }
   .chips { display:flex; gap:6px; flex-wrap:wrap; padding:0 22px 12px; }
   .chip { color:var(--dim); border:1px solid rgba(255,255,255,.15); border-radius:999px; padding:3px 12px;
     font-size:12px; cursor:pointer; text-decoration:none; }
   .chip.on { color:#171310; background:var(--brass); border-color:var(--brass); }
   #grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:14px; padding:0 22px 40px; }
-  .card { background:#211b16; border:1px solid rgba(255,255,255,.06); border-radius:8px; overflow:hidden; }
+  .card { background:#211b16; border:1px solid rgba(201,162,39,.12); border-radius:4px; overflow:hidden;
+    transition:border-color .15s, box-shadow .15s; }
+  .card:hover { border-color:rgba(201,162,39,.4); box-shadow:0 4px 20px rgba(0,0,0,.4); }
   .card img { width:100%; aspect-ratio:1; object-fit:cover; display:block; cursor:pointer; }
+  @media (max-width:640px){ .tagline{display:none} .wordmark{font-size:17px} }
   .card .meta { padding:8px 10px 10px; }
   .card a.t { color:var(--ink); text-decoration:none; font-size:13px; display:block; }
   .card a.t:hover { color:var(--brass); }
@@ -35,7 +55,7 @@
 </head>
 <body>
 <header>
-  <a class="wordmark" href="/">The Designer <em>Library</em></a>
+  <div><a class="wordmark" href="/">The Designer <em>Library</em></a><span class="tagline">Every collection, shelved.</span></div>
   <a class="chip" href="/">← back to the shelves</a>
 </header>
 <div class="controls">
@@ -44,6 +64,7 @@
     <option value="newest">Newest</option>
     <option value="title">Title A→Z</option>
     <option value="vendor">Brand A→Z</option>
+    <option value="collection">Collection A→Z</option>
     <option value="light-dark">Light → Dark</option>
     <option value="dark-light">Dark → Light</option>
     <option value="wheel">Color Wheel</option>
@@ -65,9 +86,15 @@ const state = {
   vendor: P.get('vendor') || '', hue: P.get('hue') || '', style: P.get('style') || '',
   collection: P.get('collection') || '', page: 1, busy: false, done: false, total: 0,
 };
+const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
 $('#sort').value = state.sort; $('#q').value = state.q;
-$('#density').value = localStorage.tdl_cols || 5;
-document.documentElement.style.setProperty('--cols', $('#density').value);
+// first visit on a phone: 5 columns = 58px cards. Default by viewport, and
+// clamp a desktop-saved value so a synced localStorage can't wreck mobile.
+const defCols = window.innerWidth < 640 ? 2 : 5;
+let cols = parseInt(localStorage.tdl_cols) || defCols;
+if (window.innerWidth < 640) cols = Math.min(cols, 3);
+$('#density').value = cols;
+document.documentElement.style.setProperty('--cols', cols);
 
 function syncUrl() {
   const p = new URLSearchParams();
@@ -82,38 +109,47 @@ function syncUrl() {
 
 async function loadFacets() {
   const f = await fetch('/api/facets').then(r => r.json());
-  $('#fVendor').innerHTML += f.vendors.map(v => `<option value="${v.value}">${v.value} (${v.n.toLocaleString()})</option>`).join('');
-  $('#fHue').innerHTML += f.hues.map(v => `<option value="${v.value}">${v.value} (${v.n.toLocaleString()})</option>`).join('');
-  $('#fStyle').innerHTML += f.styles.map(v => `<option value="${v.value}">${v.value} (${v.n.toLocaleString()})</option>`).join('');
+  const fill = (sel, list) => sel.append(...list.map(v => new Option(`${v.value} (${v.n.toLocaleString()})`, v.value)));
+  fill($('#fVendor'), f.vendors); fill($('#fHue'), f.hues); fill($('#fStyle'), f.styles);
   $('#fVendor').value = state.vendor; $('#fHue').value = state.hue; $('#fStyle').value = state.style;
 }
 
 async function loadPage() {
   if (state.busy || state.done) return;
   state.busy = true;
-  const p = new URLSearchParams({ sort: state.sort, page: state.page, limit: 48 });
-  for (const k of ['q', 'vendor', 'hue', 'style', 'collection']) if (state[k]) p.set(k, state[k]);
-  const data = await fetch('/api/products?' + p).then(r => r.json());
-  state.total = data.total;
-  const frag = document.createDocumentFragment();
-  for (const pr of data.products) {
-    const d = document.createElement('div'); d.className = 'card';
-    d.innerHTML = `
-      <img loading="lazy" src="${pr.image}" alt="${pr.title.replace(/"/g, '&quot;')}">
-      <div class="meta">
-        <a class="t" href="${pr.url}" target="_blank" rel="nofollow sponsored noopener">${pr.hex ? `<span class="dot" style="background:${pr.hex}" title="more in this hue"></span>` : ''}${pr.title}</a>
-        <div class="v"><a href="/browse?vendor=${encodeURIComponent(pr.vendor)}">${pr.vendor}</a>${pr.collection ? ` · <a href="/browse?vendor=${encodeURIComponent(pr.vendor)}&collection=${encodeURIComponent(pr.collection)}">${pr.collection}</a>` : ''}</div>
-      </div>`;
-    d.querySelector('img').onclick = () => window.open(pr.url, '_blank', 'noopener');
-    if (pr.hue) d.querySelector('.dot')?.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); state.hue = pr.hue; reset(); });
-    frag.appendChild(d);
+  // without the try/finally, one failed fetch left busy=true forever and
+  // silently killed infinite scroll for the rest of the session
+  try {
+    const p = new URLSearchParams({ sort: state.sort, page: state.page, limit: 48 });
+    for (const k of ['q', 'vendor', 'hue', 'style', 'collection']) if (state[k]) p.set(k, state[k]);
+    const r = await fetch('/api/products?' + p);
+    if (!r.ok) throw new Error('HTTP ' + r.status);
+    const data = await r.json();
+    state.total = data.total;
+    const frag = document.createDocumentFragment();
+    for (const pr of data.products) {
+      const d = document.createElement('div'); d.className = 'card';
+      d.innerHTML = `
+        <img loading="lazy" src="${esc(pr.image)}" alt="${esc(pr.title)}">
+        <div class="meta">
+          <a class="t" href="${esc(pr.url)}" target="_blank" rel="nofollow sponsored noopener">${pr.hex ? `<span class="dot" style="background:${esc(pr.hex)}" title="more in this hue"></span>` : ''}${esc(pr.title)}</a>
+          <div class="v"><a href="/browse?vendor=${encodeURIComponent(pr.vendor)}">${esc(pr.vendor)}</a>${pr.collection ? ` · <a href="/browse?vendor=${encodeURIComponent(pr.vendor)}&collection=${encodeURIComponent(pr.collection)}">${esc(pr.collection)}</a>` : ''}</div>
+        </div>`;
+      d.querySelector('img').onclick = () => window.open(pr.url, '_blank', 'noopener');
+      if (pr.hue) d.querySelector('.dot')?.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); state.hue = pr.hue; reset(); });
+      frag.appendChild(d);
+    }
+    $('#grid').appendChild(frag);
+    state.page++;
+    state.done = (state.page - 1) * 48 >= data.total;
+    $('#status').textContent = state.done
+      ? `${state.total.toLocaleString()} designs`
+      : `${Math.min((state.page - 1) * 48, state.total).toLocaleString()} of ${state.total.toLocaleString()} designs — scroll for more`;
+  } catch {
+    $('#status').textContent = 'trouble loading — scroll to retry';
+  } finally {
+    state.busy = false;
   }
-  $('#grid').appendChild(frag);
-  state.page++; state.busy = false;
-  state.done = (state.page - 1) * 48 >= data.total;
-  $('#status').textContent = state.done
-    ? `${state.total.toLocaleString()} designs`
-    : `${Math.min((state.page - 1) * 48, state.total).toLocaleString()} of ${state.total.toLocaleString()} designs — scroll for more`;
 }
 
 function reset() { $('#grid').innerHTML = ''; state.page = 1; state.done = false; syncUrl(); loadPage(); }
@@ -127,6 +163,9 @@ $('#density').oninput = (e) => { localStorage.tdl_cols = e.target.value; documen
 window.addEventListener('scroll', () => {
   if (window.innerHeight + window.scrollY > document.body.offsetHeight - 900) loadPage();
 });
+// the server SSRs the first 48 cards for crawlers — clear them before the
+// client render so the same products don't appear twice
+$('#grid').innerHTML = '';
 loadFacets().then(() => { syncUrl(); loadPage(); });
 </script>
 <script async src="https://www.googletagmanager.com/gtag/js?id=G-LPY7H5RZ20"></script>
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..38ab4f7
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
+  <rect width="64" height="64" rx="12" fill="#171310"/>
+  <!-- three shelved book spines, middle one pulled up — brass on walnut -->
+  <rect x="14" y="18" width="9" height="30" rx="1.5" fill="#c9a227"/>
+  <rect x="27" y="12" width="9" height="36" rx="1.5" fill="#efe8db"/>
+  <rect x="40" y="18" width="9" height="30" rx="1.5" fill="#8a6f1d"/>
+  <rect x="12" y="50" width="40" height="3" rx="1.5" fill="#3a2e24"/>
+</svg>
diff --git a/public/index.html b/public/index.html
index 9a54bdc..7e69f9a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6,7 +6,46 @@
 <title>The Designer Library — Wallcovering Collections, Brands, Styles & Hues</title>
 <meta name="description" content="A 3D library of designer wallcoverings — browse 84,000+ designs as books on shelves, organized by hue, brand, collection and style. Pull a book, flip through, click into the store.">
 <link rel="canonical" href="https://thedesignerlibrary.com/">
-<script type="importmap">{ "imports": { "three": "/vendor/three.module.js" } }</script>
+<link rel="icon" href="/favicon.svg" type="image/svg+xml">
+<meta property="og:type" content="website">
+<meta property="og:url" content="https://thedesignerlibrary.com/">
+<meta property="og:title" content="The Designer Library — Wallcovering Collections, Brands, Styles & Hues">
+<meta property="og:description" content="A 3D library of designer wallcoverings — browse 84,000+ designs as books on shelves, organized by hue, brand, collection and style.">
+<meta property="og:image" content="https://thedesignerlibrary.com/og-image.jpg">
+<meta property="og:image:width" content="1200">
+<meta property="og:image:height" content="630">
+<meta property="og:site_name" content="The Designer Library">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="The Designer Library — Wallcovering Collections, Brands, Styles & Hues">
+<meta name="twitter:description" content="A 3D library of designer wallcoverings — 84,000+ designs organized by hue, brand, collection and style.">
+<meta name="twitter:image" content="https://thedesignerlibrary.com/og-image.jpg">
+<script type="application/ld+json">
+{
+  "@context": "https://schema.org",
+  "@graph": [
+    {
+      "@type": "WebSite",
+      "@id": "https://thedesignerlibrary.com/#website",
+      "url": "https://thedesignerlibrary.com/",
+      "name": "The Designer Library",
+      "description": "A 3D library of designer wallcoverings — 84,000+ designs organized by hue, brand, collection and style.",
+      "potentialAction": {
+        "@type": "SearchAction",
+        "target": { "@type": "EntryPoint", "urlTemplate": "https://thedesignerlibrary.com/browse?q={search_term_string}" },
+        "query-input": "required name=search_term_string"
+      }
+    },
+    {
+      "@type": "Organization",
+      "@id": "https://thedesignerlibrary.com/#organization",
+      "name": "The Designer Library",
+      "url": "https://thedesignerlibrary.com/",
+      "sameAs": ["https://www.designerwallcoverings.com"]
+    }
+  ]
+}
+</script>
+<script type="importmap">{ "imports": { "three": "/vendor/three-0.160.0.module.js" } }</script>
 <style>
   :root { --bg:#171310; --ink:#efe8db; --dim:#a99e8d; --brass:#c9a227; --walnut:#3a2e24; }
   * { box-sizing:border-box; }
@@ -15,11 +54,14 @@
   #shelf { position:fixed; inset:0; }
   header { position:fixed; top:0; left:0; right:0; z-index:10; display:flex; align-items:center;
     justify-content:space-between; padding:16px 22px; pointer-events:none; }
-  .wordmark { pointer-events:auto; text-decoration:none; color:var(--ink); letter-spacing:.16em;
-    font-size:19px; font-weight:600; text-transform:uppercase; }
+  .wordmark { pointer-events:auto; text-decoration:none; color:var(--ink); letter-spacing:.14em;
+    font-size:clamp(30px, 3.2vw, 48px); font-weight:600; text-transform:uppercase; white-space:nowrap; }
   .wordmark em { color:var(--brass); font-style:normal; }
-  .tagline { font-style:italic; color:var(--dim); font-size:13px; margin-left:12px; letter-spacing:.04em; }
-  #burger { pointer-events:auto; background:none; border:0; color:var(--ink); font-size:24px; cursor:pointer; padding:6px 10px; }
+  .tagline { font-style:italic; color:var(--dim); font-size:13px; margin-left:14px; letter-spacing:.04em; }
+  #burger { pointer-events:auto; background:none; border:1px solid rgba(201,162,39,.35); border-radius:50%;
+    width:42px; height:42px; display:flex; align-items:center; justify-content:center;
+    color:var(--ink); font-size:18px; cursor:pointer; }
+  #burger:hover { border-color:var(--brass); color:var(--brass); }
   nav#menu { position:fixed; top:0; right:0; bottom:0; width:270px; background:#211b16; z-index:30;
     transform:translateX(100%); transition:transform .25s ease; padding:70px 26px; }
   nav#menu.open { transform:none; }
@@ -27,7 +69,7 @@
     border-bottom:1px solid rgba(255,255,255,.07); }
   nav#menu a:hover { color:var(--brass); }
   #menuClose { position:absolute; top:16px; right:18px; background:none; border:0; color:var(--dim); font-size:22px; cursor:pointer; }
-  .bar { position:fixed; top:62px; left:0; right:0; z-index:9; display:flex; gap:8px; justify-content:center;
+  .bar { position:fixed; top:96px; left:0; right:0; z-index:9; display:flex; gap:8px; justify-content:center;
     align-items:center; flex-wrap:wrap; padding:0 16px; }
   .tab { pointer-events:auto; background:rgba(33,27,22,.85); color:var(--dim); border:1px solid rgba(201,162,39,.25);
     border-radius:999px; padding:7px 18px; font:600 12px Georgia, serif; letter-spacing:.14em; cursor:pointer; }
@@ -39,15 +81,25 @@
   input#search { background:rgba(33,27,22,.85); color:var(--ink); border:1px solid rgba(255,255,255,.14);
     border-radius:999px; padding:7px 14px; font:13px Georgia, serif; width:180px; }
   input#search::placeholder { color:var(--dim); }
-  footer { position:fixed; bottom:0; left:0; right:0; z-index:8; display:flex; justify-content:space-between;
-    align-items:center; padding:10px 22px; color:var(--dim); font-size:12px; font-style:italic; pointer-events:none; }
+  /* footer content lives LEFT so the DW-network ad card (bottom-right) never
+     covers the count or the interaction hints */
+  footer { position:fixed; bottom:0; left:0; right:340px; z-index:8; display:flex; justify-content:flex-start;
+    gap:18px; align-items:center; padding:10px 22px; color:var(--dim); font-size:11px; font-style:italic;
+    letter-spacing:.06em; opacity:.8; pointer-events:none; }
   footer a { pointer-events:auto; color:var(--dim); }
   #count { pointer-events:none; }
   #reader { position:fixed; bottom:44px; left:0; right:0; z-index:8; text-align:center; color:var(--dim);
     font-size:13px; display:none; }
   #loading { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; z-index:5;
     color:var(--dim); font-style:italic; letter-spacing:.08em; }
-  @media (max-width:640px){ .tagline{display:none} input#search{width:120px} }
+  @media (max-width:640px){
+    .tagline{display:none} input#search{width:120px}
+    .wordmark{font-size:17px; letter-spacing:.14em;}
+    #burger{width:36px; height:36px; font-size:15px;}
+    .bar{top:58px;}
+    footer{bottom:96px; right:0; flex-wrap:wrap; gap:6px;}
+    #reader{bottom:140px;}
+  }
 </style>
 </head>
 <body>
@@ -77,7 +129,7 @@
 <div id="loading">fetching the library…</div>
 <div id="reader"></div>
 <footer>
-  <span>scroll / drag the shelf · click a spine to open · ←/→ flip · Esc reshelves</span>
+  <span id="hints">scroll / drag the shelf · click a spine to open · ←/→ flip · Esc reshelves</span>
   <span id="count"></span>
 </footer>
 
@@ -88,9 +140,11 @@ const $ = (s) => document.querySelector(s);
 const shelfEl = $('#shelf'), loading = $('#loading'), reader = $('#reader'), countEl = $('#count');
 let shelf = null, currentBooks = [], currentShelf = null;
 
+const TOUCH = matchMedia('(pointer: coarse)').matches;
 const THEME = {
   background: '#171310', shelfColor: '#3a2e24', accent: '#c9a227',
   onOpen(book) { reader.style.display = 'block'; reader.textContent = `${book.title} — ${book.subtitle || ''}`; },
+  onClose() { reader.style.display = 'none'; },
   onPageChange(book, i) {
     const p = book.pages[i];
     reader.textContent = p ? `${book.title} · ${p.title}` : book.title;
@@ -109,7 +163,7 @@ async function loadShelf(name, brand) {
   if (name === 'collections' && !brand) {
     // first entry into collections: populate the brand picker, show the first brand
     const pick = $('#brandPick');
-    pick.innerHTML = data.brands.map(b => `<option>${b}</option>`).join('');
+    pick.replaceChildren(...data.brands.map(b => new Option(b, b)));
     pick.classList.add('show');
     return loadShelf('collections', data.brands[0]);
   }
@@ -121,9 +175,16 @@ async function loadShelf(name, brand) {
   reader.style.display = 'none';
   if (currentBooks.length) shelf = new BookshelfCatalog(shelfEl, currentBooks, THEME);
   loading.style.display = 'none';
-  const total = currentBooks.reduce((n, b) => n + (parseInt((b.subtitle || '').replace(/,/g, '')) || 0), 0);
+  // design count = the LAST number in the subtitle — collections subtitles are
+  // "Brand · 12", hues/brands are "23,799 designs"; a leading parseInt gets NaN
+  const total = currentBooks.reduce((n, b) => {
+    const nums = (b.subtitle || '').match(/[\d,]+/g);
+    return n + (nums ? parseInt(nums[nums.length - 1].replace(/,/g, ''), 10) || 0 : 0);
+  }, 0);
   countEl.textContent = `${currentBooks.length} books` + (total ? ` · ${total.toLocaleString()} designs` : '');
-  $('#bookNames').innerHTML = currentBooks.map(b => `<option value="${b.title.replace(/"/g, '&quot;')}">`).join('');
+  $('#bookNames').replaceChildren(...currentBooks.map(b => {
+    const o = document.createElement('option'); o.value = b.title; return o;
+  }));
   document.querySelectorAll('.tab').forEach(t => t.classList.toggle('on', t.dataset.shelf === name));
   if (window.gtag) gtag('event', 'shelf_view', { shelf: name, brand: brand || '' });
   history.replaceState(null, '', name === 'hues' ? '/' : `/#${name}${brand ? ':' + encodeURIComponent(brand) : ''}`);
@@ -132,9 +193,17 @@ async function loadShelf(name, brand) {
 document.querySelectorAll('.tab').forEach(t => t.onclick = () => loadShelf(t.dataset.shelf));
 $('#brandPick').onchange = (e) => loadShelf('collections', e.target.value);
 $('#search').addEventListener('change', (e) => {
-  const i = currentBooks.findIndex(b => b.title.toLowerCase() === e.target.value.toLowerCase());
+  const term = e.target.value.trim().toLowerCase();
+  if (!term) return;
+  // exact, then prefix, then substring against the current shelf's books;
+  // no book match → fall through to a full catalog search on /browse
+  let i = currentBooks.findIndex(b => b.title.toLowerCase() === term);
+  if (i < 0) i = currentBooks.findIndex(b => b.title.toLowerCase().startsWith(term));
+  if (i < 0) i = currentBooks.findIndex(b => b.title.toLowerCase().includes(term));
   if (i >= 0 && shelf) { shelf.goToBook(i); setTimeout(() => shelf.openBook(i), 600); }
+  else location.href = '/browse?q=' + encodeURIComponent(e.target.value.trim());
 });
+if (TOUCH) $('#hints').textContent = 'drag the shelf · tap a spine to open · tap the page to flip · tap outside the book to reshelve';
 $('#burger').onclick = () => $('#menu').classList.add('open');
 $('#menuClose').onclick = () => $('#menu').classList.remove('open');
 document.querySelectorAll('#menu a[data-shelf]').forEach(a => a.onclick = (e) => {
diff --git a/public/og-image.jpg b/public/og-image.jpg
new file mode 100644
index 0000000..a4e2e2f
Binary files /dev/null and b/public/og-image.jpg differ
diff --git a/public/vendor/three.module.js b/public/vendor/three-0.160.0.module.js
similarity index 100%
rename from public/vendor/three.module.js
rename to public/vendor/three-0.160.0.module.js
diff --git a/server.js b/server.js
index ca671b6..4afa30d 100644
--- a/server.js
+++ b/server.js
@@ -7,13 +7,38 @@ const path = require('path');
 
 const CFG = JSON.parse(fs.readFileSync(path.join(__dirname, 'site.config.json'), 'utf8'));
 const PORT = process.env.PORT || CFG.port || 9807;
+const ORIGIN = 'https://thedesignerlibrary.com';
 
 let LIB = { brands: [], collectionsByBrand: {}, styles: [], hues: [], counts: {}, builtAt: null };
 let GRID = [];
+let BROWSE_HTML = '';
+let FACETS_CACHE = null;
+const SORTED_CACHE = {};
+
+function hexToHsl(hex) {
+  if (!hex) return null;
+  const h6 = hex.replace('#', '');
+  const r = parseInt(h6.slice(0, 2), 16) / 255, g = parseInt(h6.slice(2, 4), 16) / 255, b = parseInt(h6.slice(4, 6), 16) / 255;
+  const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
+  let h = 0;
+  if (d) {
+    if (max === r) h = ((g - b) / d) % 6; else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4;
+    h = (h * 60 + 360) % 360;
+  }
+  const l = (max + min) / 2;
+  return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };
+}
+
 function loadData() {
   try {
     LIB = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'library.json'), 'utf8'));
     GRID = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
+    // precompute HSL once — the color sorts would otherwise re-parse hex
+    // strings ~1.4M times per unfiltered request (85k rows × log n compares)
+    for (const p of GRID) p._hsl = hexToHsl(p.x);
+    FACETS_CACHE = null;
+    for (const k of Object.keys(SORTED_CACHE)) delete SORTED_CACHE[k];
+    BROWSE_HTML = fs.readFileSync(path.join(__dirname, 'public', 'browse.html'), 'utf8');
     console.log(`library loaded: ${GRID.length} products, built ${LIB.builtAt}`);
   } catch (e) { console.error('data load failed:', e.message); }
 }
@@ -22,11 +47,15 @@ loadData();
 const app = express();
 app.disable('x-powered-by');
 app.use((req, res, next) => { res.setHeader('X-Frame-Options', 'SAMEORIGIN'); next(); });
+// raw JSON in a search index helps no one — keep crawlers on the HTML pages
+app.use('/api', (req, res, next) => { res.setHeader('X-Robots-Tag', 'noindex, nofollow'); next(); });
 
 app.get('/api/health', (_req, res) => res.json({ ok: true, products: GRID.length, builtAt: LIB.builtAt }));
 
-// per-shelf datasets — the landing view fetches only its own shelf
+// per-shelf datasets — the landing view fetches only its own shelf.
+// Data is build-time static, so let Cloudflare edge-cache it (s-maxage).
 app.get('/api/shelf/:name', (req, res) => {
+  res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=3600');
   const n = req.params.name;
   if (n === 'brands') return res.json({ books: LIB.brands });
   if (n === 'styles') return res.json({ books: LIB.styles });
@@ -40,63 +69,113 @@ app.get('/api/shelf/:name', (req, res) => {
 });
 
 // ---------- /browse grid API ----------
-function hexToHsl(hex) {
-  if (!hex) return null;
-  const h6 = hex.replace('#', '');
-  const r = parseInt(h6.slice(0, 2), 16) / 255, g = parseInt(h6.slice(2, 4), 16) / 255, b = parseInt(h6.slice(4, 6), 16) / 255;
-  const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
-  let h = 0;
-  if (d) {
-    if (max === r) h = ((g - b) / d) % 6; else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4;
-    h = (h * 60 + 360) % 360;
-  }
-  const l = (max + min) / 2;
-  return { h, s: d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)), l };
-}
 const SORTS = {
   newest: (a, b) => (b.d || '').localeCompare(a.d || ''),
   title: (a, b) => a.t.localeCompare(b.t),
   vendor: (a, b) => a.v.localeCompare(b.v) || a.t.localeCompare(b.t),
-  'light-dark': (a, b) => (hexToHsl(b.x)?.l ?? -1) - (hexToHsl(a.x)?.l ?? -1),
-  'dark-light': (a, b) => (hexToHsl(a.x)?.l ?? 2) - (hexToHsl(b.x)?.l ?? 2),
-  wheel: (a, b) => (hexToHsl(a.x)?.h ?? 999) - (hexToHsl(b.x)?.h ?? 999),
+  collection: (a, b) => (a.c || '￿').localeCompare(b.c || '￿') || a.t.localeCompare(b.t),
+  'light-dark': (a, b) => (b._hsl?.l ?? -1) - (a._hsl?.l ?? -1),
+  'dark-light': (a, b) => (a._hsl?.l ?? 2) - (b._hsl?.l ?? 2),
+  wheel: (a, b) => (a._hsl?.h ?? 999) - (b._hsl?.h ?? 999),
 };
 
-app.get('/api/products', (req, res) => {
-  const { vendor, collection, style, hue, q } = req.query;
-  const sort = SORTS[req.query.sort] ? req.query.sort : 'newest';
-  const page = Math.max(1, parseInt(req.query.page) || 1);
-  const limit = Math.min(96, Math.max(1, parseInt(req.query.limit) || 48));
-  let list = GRID;
-  if (vendor) list = list.filter(p => p.v === vendor);
-  if (collection) list = list.filter(p => p.c === collection);
-  if (style) list = list.filter(p => p.st === style);
-  if (hue) list = list.filter(p => p.hu === hue);
-  if (q) {
-    const terms = q.toLowerCase().split(/\s+/).filter(Boolean);
-    list = list.filter(p => { const hay = (p.t + ' ' + p.v + ' ' + (p.c || '')).toLowerCase(); return terms.every(t => hay.includes(t)); });
+// unfiltered sorted views are identical for every visitor — sort once, slice forever
+function getSorted(sort) {
+  if (!SORTED_CACHE[sort]) SORTED_CACHE[sort] = [...GRID].sort(SORTS[sort]);
+  return SORTED_CACHE[sort];
+}
+
+function queryProducts(q) {
+  const { vendor, collection, style, hue } = q;
+  const sort = SORTS[q.sort] ? q.sort : 'newest';
+  const page = Math.max(1, parseInt(q.page) || 1);
+  const limit = Math.min(96, Math.max(1, parseInt(q.limit) || 48));
+  const term = (q.q || '').trim();
+  const filtered = vendor || collection || style || hue || term;
+  let sorted;
+  if (!filtered) {
+    sorted = getSorted(sort);
+  } else {
+    let list = GRID;
+    if (vendor) list = list.filter(p => p.v === vendor);
+    if (collection) list = list.filter(p => p.c === collection);
+    if (style) list = list.filter(p => p.st === style);
+    if (hue) list = list.filter(p => p.hu === hue);
+    if (term) {
+      const terms = term.toLowerCase().split(/\s+/).filter(Boolean);
+      list = list.filter(p => { const hay = (p.t + ' ' + p.v + ' ' + (p.c || '')).toLowerCase(); return terms.every(t => hay.includes(t)); });
+    }
+    sorted = [...list].sort(SORTS[sort]);
   }
-  const sorted = [...list].sort(SORTS[sort]);
   const start = (page - 1) * limit;
-  res.json({
+  return {
     total: sorted.length, page, limit,
     products: sorted.slice(start, start + limit).map(p => ({
       title: p.t, vendor: p.v, handle: p.h, image: p.i, hex: p.x, hue: p.hu, style: p.st, collection: p.c,
       url: `${CFG.storeBase}/products/${p.h}`,
     })),
-  });
+  };
+}
+
+app.get('/api/products', (req, res) => {
+  res.setHeader('Cache-Control', 'public, max-age=60, s-maxage=600');
+  res.json(queryProducts(req.query));
 });
 
 app.get('/api/facets', (req, res) => {
-  const count = (key) => {
-    const m = new Map();
-    for (const p of GRID) { const v = p[key]; if (v) m.set(v, (m.get(v) || 0) + 1); }
-    return [...m.entries()].sort((a, b) => b[1] - a[1]).map(([value, n]) => ({ value, n }));
-  };
-  res.json({ vendors: count('v').slice(0, 60), hues: count('hu'), styles: count('st') });
+  if (!FACETS_CACHE) {
+    const count = (key) => {
+      const m = new Map();
+      for (const p of GRID) { const v = p[key]; if (v) m.set(v, (m.get(v) || 0) + 1); }
+      return [...m.entries()].sort((a, b) => b[1] - a[1]).map(([value, n]) => ({ value, n }));
+    };
+    FACETS_CACHE = { vendors: count('v').slice(0, 60), hues: count('hu'), styles: count('st') };
+  }
+  res.setHeader('Cache-Control', 'public, max-age=300, s-maxage=3600');
+  res.json(FACETS_CACHE);
+});
+
+// ---------- SEO surface ----------
+app.get('/robots.txt', (_req, res) => {
+  res.type('text/plain').send(`User-agent: *\nAllow: /\nDisallow: /api/\nSitemap: ${ORIGIN}/sitemap.xml\n`);
+});
+
+app.get('/sitemap.xml', (_req, res) => {
+  // just the two real pages — filter URLs all serve the same client-rendered
+  // shell, so listing them would hand Google thousands of duplicate pages
+  const urls = [
+    { loc: `${ORIGIN}/`, changefreq: 'weekly', priority: '1.0' },
+    { loc: `${ORIGIN}/browse`, changefreq: 'daily', priority: '0.9' },
+  ];
+  res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+${urls.map(u => `<url><loc>${u.loc}</loc><changefreq>${u.changefreq}</changefreq><priority>${u.priority}</priority></url>`).join('\n')}
+</urlset>`);
+});
+
+const escHtml = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+
+// /browse with the first 48 products server-rendered — the only indexable
+// product content on the site (the 3D landing is pixels to a crawler).
+// The client script re-renders the grid on load, so hydration is a wash.
+app.get('/browse', (req, res) => {
+  res.setHeader('Cache-Control', 'no-cache');
+  let html = BROWSE_HTML;
+  try {
+    const first = queryProducts({ sort: 'newest', limit: 48 });
+    const cards = first.products.map(p => `<div class="card"><a href="${escHtml(p.url)}" target="_blank" rel="nofollow sponsored noopener"><img loading="lazy" src="${escHtml(p.image)}" alt="${escHtml(p.title)}" width="300" height="300"></a><div class="meta"><a class="t" href="${escHtml(p.url)}" target="_blank" rel="nofollow sponsored noopener">${escHtml(p.title)}</a><div class="v">${escHtml(p.vendor)}${p.collection ? ' · ' + escHtml(p.collection) : ''}</div></div></div>`).join('');
+    html = html.replace('<div id="grid"></div>', `<div id="grid">${cards}</div>`);
+  } catch { /* fall through to the empty shell */ }
+  res.send(html);
 });
 
-app.use(express.static(path.join(__dirname, 'public'), { maxAge: '1h', extensions: ['html'] }));
-app.get('/browse', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'browse.html')));
+app.use(express.static(path.join(__dirname, 'public'), {
+  extensions: ['html'],
+  setHeaders(res, filePath) {
+    if (filePath.endsWith('.html')) res.setHeader('Cache-Control', 'no-cache');           // deploys visible immediately
+    else if (filePath.includes(`${path.sep}vendor${path.sep}`)) res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); // versioned filenames
+    else res.setHeader('Cache-Control', 'public, max-age=86400');
+  },
+}));
 
 app.listen(PORT, () => console.log(`The Designer Library on :${PORT}`));

← fff9204 pixel: switch to the DW NETWORK pixel 1431180262113856 (Stev  ·  back to Thedesignerlibrary  ·  contrarian fixes: hex format guard on style attr, SSR cards 122f00a →