← back to Bestwallpaperplace
Untrack snapshot files and 404-guard .bak/.pre- static paths
b870cb517e127152772840edba09b28d8440b372 · 2026-05-19 08:17:05 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M .gitignoreD public/index.html.bak.20260513T193424ZD public/index.html.pre-sort-frontendM server.jsD server.js.pre-sort-skill
Diff
commit b870cb517e127152772840edba09b28d8440b372
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 19 08:17:05 2026 -0700
Untrack snapshot files and 404-guard .bak/.pre- static paths
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
.gitignore | 3 +
public/index.html.bak.20260513T193424Z | 533 ---------------------------------
public/index.html.pre-sort-frontend | 430 --------------------------
server.js | 9 +
server.js.pre-sort-skill | 119 --------
5 files changed, 12 insertions(+), 1082 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4a0b3ea..a90a60e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,6 @@ tmp/
dist/
build/
.next/
+*.bak
+*.bak.*
+*.pre-*
diff --git a/public/index.html.bak.20260513T193424Z b/public/index.html.bak.20260513T193424Z
deleted file mode 100644
index 2140a72..0000000
--- a/public/index.html.bak.20260513T193424Z
+++ /dev/null
@@ -1,533 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<!-- GA4 gtag (auto) -->
-<script async src="https://www.googletagmanager.com/gtag/js?id=G-SSNGG2BQC3"></script>
-<script>
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', 'G-SSNGG2BQC3');
-</script>
-<!-- /GA4 gtag -->
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>Best Wallpaper Place — Hand-picked, the very best wallpaper</title>
-<meta name="description" content="Best Wallpaper Place is a curated showcase of the finest wallpaper patterns — hand-picked from premium vendors. The patterns we keep coming back to.">
-<meta name="theme-color" content="#faf8f3">
-<link rel="canonical" href="https://bestwallpaperplace.com/">
-<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ctext y='20' font-size='22'%3E%E2%98%85%3C/text%3E%3C/svg%3E">
-<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
-
-<meta property="og:type" content="website">
-<meta property="og:url" content="https://bestwallpaperplace.com/">
-<meta property="og:title" content="Best Wallpaper Place — Hand-picked wallpaper favorites">
-<meta property="og:description" content="A curated showcase of the finest wallpaper patterns. The ones we keep coming back to.">
-<meta name="twitter:card" content="summary_large_image">
-
-<script>
-(function(){ try { var t = localStorage.getItem('bwp_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
-</script>
-<style>
-:root {
- --bg: #faf8f3;
- --paper: #1a1a1a;
- --muted: #7a7a7a;
- --line: #e5e0d6;
- --accent: #b8860b;
- --bg-soft: #f0ebe0;
- --cols: 5;
-}
-html[data-theme="dark"] {
- --bg: #14110d;
- --paper: #faf8f3;
- --muted: #9a8f7a;
- --line: rgba(255,255,255,0.10);
- --accent: #d4a017;
- --bg-soft: #1f1a14;
-}
-* { margin:0; padding:0; box-sizing:border-box; }
-html { scroll-behavior:smooth; }
-body {
- font-family:'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
- color:var(--paper);
- background:var(--bg);
- -webkit-font-smoothing:antialiased;
- min-height:100vh;
-}
-h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif; font-weight:500; letter-spacing:-0.01em; }
-
-/* Header */
-header {
- position:sticky; top:0; z-index:100;
- display:flex; justify-content:space-between; align-items:center;
- padding:18px 32px;
- background:color-mix(in srgb, var(--bg) 92%, transparent);
- backdrop-filter:blur(12px);
- border-bottom:1px solid var(--line);
-}
-.brand { display:flex; align-items:center; gap:12px; text-decoration:none; color:var(--paper); }
-.brand .star { font-size:22px; color:var(--accent); line-height:1; }
-.brand .name { font-family:'Cormorant Garamond', serif; font-size:20px; font-weight:600; letter-spacing:0.02em; }
-.brand .name sub { font-size:9px; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted); display:block; margin-top:2px; font-family:'Inter',sans-serif; font-weight:600; }
-.h-actions { display:flex; align-items:center; gap:14px; }
-.theme-toggle {
- background:transparent; border:1px solid var(--line);
- width:36px; height:36px; border-radius:50%;
- cursor:pointer; color:var(--paper);
- display:inline-flex; align-items:center; justify-content:center;
- font-size:15px; transition:all .15s; padding:0;
-}
-.theme-toggle:hover { border-color:var(--accent); color:var(--accent); }
-
-/* Hero */
-.hero {
- padding:96px 32px 64px;
- text-align:center;
- border-bottom:1px solid var(--line);
-}
-.hero .eyebrow {
- font-size:11px; letter-spacing:0.4em; text-transform:uppercase;
- font-weight:700; color:var(--muted); margin-bottom:18px;
-}
-.hero h1 {
- font-size:clamp(48px, 7vw, 96px);
- line-height:1.05;
- margin:0 auto 18px;
- max-width:1100px;
-}
-.hero h1 .accent { color:var(--accent); font-style:italic; }
-.hero .sub {
- font-size:clamp(15px, 1.6vw, 19px);
- color:var(--muted); max-width:620px; margin:0 auto 32px;
- line-height:1.55;
-}
-.hero .stats {
- display:flex; justify-content:center; gap:48px; flex-wrap:wrap;
- font-size:11px; letter-spacing:0.22em; text-transform:uppercase;
- font-weight:700; color:var(--muted);
-}
-.hero .stats span b { display:block; font-family:'Cormorant Garamond',serif; font-size:28px; font-weight:500; color:var(--paper); letter-spacing:0; text-transform:none; margin-top:4px; }
-
-/* Toolbar */
-.section { padding:48px 32px 96px; max-width:1600px; margin:0 auto; }
-.toolbar {
- display:flex; align-items:center; gap:16px;
- flex-wrap:wrap;
- margin-bottom:24px; padding-bottom:18px;
- border-bottom:1px solid var(--line);
-}
-.toolbar .search {
- flex:0 1 280px; display:flex; align-items:center; gap:10px;
- border-bottom:1px solid var(--line); padding:6px 0;
-}
-.toolbar .search input {
- flex:1; background:transparent; border:0; color:var(--paper);
- font-family:inherit; font-size:13px; outline:none; letter-spacing:0.02em;
-}
-.toolbar .search input::placeholder { color:var(--muted); }
-.toolbar .ctl {
- display:flex; align-items:center; gap:10px;
- font-size:10px; letter-spacing:0.28em; text-transform:uppercase;
- font-weight:700; color:var(--muted);
-}
-.toolbar select {
- background:transparent; border:1px solid var(--line);
- padding:8px 12px; color:var(--paper);
- font-family:inherit; font-size:11px;
- letter-spacing:0.18em; text-transform:uppercase; font-weight:600;
- cursor:pointer; outline:none;
-}
-.toolbar select:hover { border-color:var(--accent); }
-.density input[type=range] {
- -webkit-appearance:none; appearance:none;
- width:140px; height:1px; background:var(--line);
- outline:none;
-}
-.density input[type=range]::-webkit-slider-thumb {
- -webkit-appearance:none; appearance:none;
- width:14px; height:14px; background:var(--accent);
- cursor:pointer; border-radius:50%;
-}
-.density input[type=range]::-moz-range-thumb {
- width:14px; height:14px; background:var(--accent);
- cursor:pointer; border-radius:50%; border:0;
-}
-.stat-line {
- font-size:10px; letter-spacing:0.32em; text-transform:uppercase;
- font-weight:600; color:var(--muted); margin-bottom:24px;
-}
-
-/* Grid */
-.grid {
- display:grid; grid-template-columns:repeat(var(--cols), 1fr);
- gap:2px;
-}
-.card {
- position:relative; aspect-ratio:1/1.15;
- cursor:pointer; overflow:hidden;
- background:var(--bg-soft);
- transition:transform 0.4s cubic-bezier(0.2,0.8,0.2,1);
-}
-.card:hover { transform:scale(1.04); z-index:5; }
-.card img {
- width:100%; height:100%; object-fit:cover; display:block;
- transition:transform 0.4s ease;
-}
-.card .overlay {
- position:absolute; left:0; right:0; bottom:0;
- padding:18px 14px 12px;
- background:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.85) 100%);
- transform:translateY(50%); opacity:0.85;
- transition:transform 0.3s ease, opacity 0.3s ease;
-}
-.card:hover .overlay { transform:translateY(0); opacity:1; }
-.card .pat {
- font-size:13px; font-weight:600; color:#fff;
- line-height:1.25;
- display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
- overflow:hidden;
-}
-.card .ven {
- font-size:9px; letter-spacing:0.18em; text-transform:uppercase;
- color:#fff; opacity:0.7; font-weight:500; margin-top:4px;
-}
-.card .actions { margin-top:10px; display:flex; gap:6px; }
-.card .sample-btn {
- flex:1; padding:7px 10px;
- background:#fff; color:#000;
- font-family:inherit; font-size:10px; letter-spacing:0.22em;
- text-transform:uppercase; font-weight:700;
- border:0; cursor:pointer; text-align:center;
- text-decoration:none;
- transition:all 0.2s;
-}
-.card .sample-btn:hover { background:var(--accent); color:#fff; }
-
-@media (max-width:1024px) { .grid { grid-template-columns:repeat(min(var(--cols), 4), 1fr); } }
-@media (max-width:720px) {
- .grid { grid-template-columns:repeat(2, 1fr); }
- .density { display:none; }
- header { padding:14px 20px; }
- .section { padding-left:20px; padding-right:20px; }
- .hero { padding:64px 20px 48px; }
-}
-
-.loading {
- text-align:center; color:var(--muted);
- padding:32px;
- font-size:10px; letter-spacing:0.32em; text-transform:uppercase; font-weight:700;
-}
-
-/* Footer */
-footer { padding:64px 32px 32px; border-top:1px solid var(--line); margin-top:32px; }
-.footer-grid {
- max-width:1400px; margin:0 auto;
- display:grid; grid-template-columns:2fr 1fr 1fr;
- gap:48px; margin-bottom:48px;
-}
-.footer-brand { font-family:'Cormorant Garamond',serif; font-size:24px; font-weight:500; margin-bottom:12px; }
-.footer-text { font-size:13px; line-height:1.6; color:var(--muted); max-width:380px; }
-.footer-col h4 {
- font-size:10px; letter-spacing:0.32em; text-transform:uppercase;
- font-weight:700; color:var(--paper); margin-bottom:18px;
- font-family:'Inter',sans-serif;
-}
-.footer-col a, .footer-col p {
- display:block; font-size:13px; color:var(--muted);
- text-decoration:none; margin-bottom:8px; transition:color 0.2s;
-}
-.footer-col a:hover { color:var(--accent); }
-.footer-bottom {
- max-width:1400px; margin:0 auto;
- padding-top:24px; border-top:1px solid var(--line);
- display:flex; justify-content:space-between; flex-wrap:wrap; gap:16px;
- font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted);
-}
-@media (max-width:720px) { .footer-grid { grid-template-columns:1fr; gap:32px; } }
-
-/* Focus */
-a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
- outline: 2px solid var(--accent); outline-offset: 2px;
-}
-</style>
-</head>
-<body>
-<header>
- <a href="/" class="brand">
- <span class="star">★</span>
- <span class="name">Best Wallpaper Place<sub>Hand-picked favorites</sub></span>
- </a>
- <div class="h-actions">
- <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark mode" title="Toggle theme">☾</button>
- </div>
-</header>
-
-<section class="hero">
- <div class="eyebrow">Curated · Independent · No sponsored placements</div>
- <h1>The wallpaper we'd put on <span class="accent">our own walls.</span></h1>
- <p class="sub">A hand-picked showcase of patterns from the world's best wallcovering houses — the ones we keep coming back to. Browse, sort, request a sample.</p>
- <div class="stats">
- <span><b id="statCount">—</b>Patterns</span>
- <span><b id="statVendors">—</b>Houses</span>
- <span><b>2026</b>Updated</span>
- </div>
-</section>
-
-<section class="section">
- <div class="toolbar">
- <div class="search">
- <svg width="16" height="16" stroke="currentColor" stroke-width="1.5" fill="none" viewBox="0 0 24 24" style="color:var(--muted);"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
- <input id="searchBox" type="text" placeholder="Search patterns, colors, vendors…">
- </div>
- <div class="ctl">
- <span>Sort</span>
- <select id="sortSelect">
- <option value="newest">Newest</option>
- <option value="light-dark">Light → Dark</option>
- <option value="dark-light">Dark → Light</option>
- <option value="wheel">Color Wheel</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>
- <option value="price-asc">Price ↑</option>
- <option value="price-desc">Price ↓</option>
- </select>
- </div>
- <div class="ctl density">
- <span>Density</span>
- <input id="densitySlider" type="range" min="2" max="8" value="5">
- </div>
- </div>
- <div class="stat-line" id="statLine">Loading…</div>
- <div class="grid" id="grid"></div>
- <div class="loading" id="loading" style="display:none;">Loading more…</div>
-</section>
-
-<footer>
- <div class="footer-grid">
- <div>
- <div class="footer-brand">Best Wallpaper Place</div>
- <p class="footer-text">A curated showcase of premium wallpaper from independent and design-house vendors. Memo samples available on every pattern. Sourced and curated by Designer Wallcoverings.</p>
- </div>
- <div class="footer-col">
- <h4>Contact</h4>
- <a href="mailto:info@bestwallpaperplace.com">info@bestwallpaperplace.com</a>
- <a href="tel:+18883734564">888-373-4564</a>
- </div>
- <div class="footer-col">
- <h4>Visit</h4>
- <p>Designer Wallcoverings</p>
- <p>15442 Ventura Bl #102</p>
- <p>Sherman Oaks, CA 91403</p>
- </div>
- </div>
- <div class="footer-bottom">
- <span>© 2026 Best Wallpaper Place</span>
- <span>A Designer Wallcoverings property</span>
- </div>
-</footer>
-
-<script>
-(function(){
- const grid = document.getElementById('grid');
- const statLine = document.getElementById('statLine');
- const loading = document.getElementById('loading');
- const sortSelect = document.getElementById('sortSelect');
- const searchBox = document.getElementById('searchBox');
- const density = document.getElementById('densitySlider');
- const themeBtn = document.getElementById('themeToggle');
-
- // theme
- function applyTheme(t) {
- document.documentElement.dataset.theme = t;
- themeBtn.textContent = t === 'dark' ? '☀' : '☾';
- try { localStorage.setItem('bwp_theme', t); } catch(e){}
- }
- applyTheme(localStorage.getItem('bwp_theme') || 'light');
- themeBtn.addEventListener('click', () => {
- applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
- });
-
- // density
- function applyCols(n) {
- document.documentElement.style.setProperty('--cols', n);
- try { localStorage.setItem('bwp_cols', n); } catch(e){}
- }
- const savedCols = parseInt(localStorage.getItem('bwp_cols') || '5', 10);
- density.value = savedCols;
- applyCols(savedCols);
- density.addEventListener('input', e => applyCols(e.target.value));
-
- // sort + search persistence
- const savedSort = localStorage.getItem('bwp_sort') || 'newest';
- sortSelect.value = savedSort;
- sortSelect.addEventListener('change', () => {
- try { localStorage.setItem('bwp_sort', sortSelect.value); } catch(e){}
- reset();
- });
- let searchTimer;
- searchBox.addEventListener('input', () => {
- clearTimeout(searchTimer);
- searchTimer = setTimeout(reset, 250);
- });
-
- // facets
- fetch('/api/facets').then(r => r.json()).then(d => {
- document.getElementById('statCount').textContent = d.total.toLocaleString();
- document.getElementById('statVendors').textContent = Object.keys(d.vendors || {}).length;
- }).catch(()=>{});
-
- // grid render + infinite scroll
- let page = 1, pages = 1, total = 0, busy = false;
- function reset() {
- page = 1; pages = 1; total = 0;
- grid.innerHTML = '';
- statLine.textContent = 'Loading…';
- fetchPage();
- }
- function card(p) {
- const handle = p.handle || p.sku || '';
- const ven = p.vendor || '';
- const title = (p.title || '').replace(/ \| .*$/, '');
- const img = p.image_url || '';
- const sampleUrl = '/sample/' + encodeURIComponent(handle);
- return `<article class="card">
- <a href="${sampleUrl}" target="_blank" rel="noopener" aria-label="View ${title}">
- <img loading="lazy" src="${img}" alt="${title.replace(/"/g,'"')}">
- </a>
- <div class="overlay">
- <div class="pat">${title.replace(/</g,'<')}</div>
- <div class="ven">${ven.replace(/</g,'<')}</div>
- <div class="actions">
- <a class="sample-btn" href="${sampleUrl}" target="_blank" rel="noopener">Request Sample</a>
- </div>
- </div>
- </article>`;
- }
- function fetchPage() {
- if (busy || page > pages) return;
- busy = true; loading.style.display = 'block';
- const q = encodeURIComponent(searchBox.value.trim());
- const s = encodeURIComponent(sortSelect.value);
- fetch(`/api/products?page=${page}&limit=30&q=${q}&sort=${s}`)
- .then(r => r.json())
- .then(d => {
- pages = d.pages; total = d.total;
- statLine.textContent = `${total.toLocaleString()} patterns · sort: ${d.sort}`;
- grid.insertAdjacentHTML('beforeend', d.items.map(card).join(''));
- page++;
- })
- .finally(() => { busy = false; loading.style.display = 'none'; });
- }
- // infinite scroll
- window.addEventListener('scroll', () => {
- if (busy) return;
- if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 600) fetchPage();
- });
- fetchPage();
-})();
-</script>
-<!-- DW-HAMBURGER-NAV v1 (canonical 2026-05-10) -->
-<style id="dw-hamburger-styles">
- /* Canonical hamburger nav — always visible top-right, all viewports.
- Standing rule (Steve, 2026-05-08): hamburger is the primary nav. */
- .dw-burger-btn {
- position: fixed; top: 18px; right: 18px; z-index: 9999;
- width: 44px; height: 44px;
- background: rgba(20,20,20,0.78); color: #f5f1e8;
- backdrop-filter: blur(8px) saturate(140%);
- -webkit-backdrop-filter: blur(8px) saturate(140%);
- border: 1px solid rgba(245,241,232,0.18); border-radius: 50%;
- cursor: pointer; padding: 0;
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- gap: 4px;
- box-shadow: 0 6px 20px rgba(0,0,0,0.35);
- transition: background 200ms, transform 200ms;
- }
- .dw-burger-btn:hover { background: rgba(20,20,20,0.92); }
- .dw-burger-btn span {
- display: block; width: 20px; height: 1.5px; background: currentColor;
- transition: transform 220ms, opacity 220ms;
- }
- .dw-burger-btn[aria-expanded="true"] span:nth-child(1) {
- transform: translateY(5.5px) rotate(45deg);
- }
- .dw-burger-btn[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
- .dw-burger-btn[aria-expanded="true"] span:nth-child(3) {
- transform: translateY(-5.5px) rotate(-45deg);
- }
- .dw-burger-panel {
- position: fixed; top: 72px; right: 18px; z-index: 9998;
- min-width: 240px; max-width: 88vw;
- background: rgba(15,15,15,0.96);
- backdrop-filter: blur(14px) saturate(140%);
- -webkit-backdrop-filter: blur(14px) saturate(140%);
- border: 1px solid rgba(245,241,232,0.12);
- border-radius: 14px;
- box-shadow: 0 18px 50px rgba(0,0,0,0.55);
- padding: 10px 0;
- display: none;
- font-family: -apple-system, "SF Pro Text", system-ui, sans-serif;
- }
- .dw-burger-panel.open { display: block; }
- .dw-burger-panel a,
- .dw-burger-panel button {
- display: block; width: 100%;
- padding: 13px 22px;
- color: #f5f1e8 !important;
- background: transparent; border: 0; cursor: pointer;
- text-decoration: none !important;
- font-size: 15px; font-weight: 400;
- text-align: left;
- letter-spacing: 0;
- border-bottom: 1px solid rgba(245,241,232,0.06);
- transition: background 160ms, color 160ms;
- box-sizing: border-box;
- }
- .dw-burger-panel a:last-child,
- .dw-burger-panel button:last-child { border-bottom: 0; }
- .dw-burger-panel a:hover,
- .dw-burger-panel button:hover {
- background: rgba(245,241,232,0.06);
- color: #ffffff !important;
- }
- @media (max-width: 480px) {
- .dw-burger-btn { top: 12px; right: 12px; width: 40px; height: 40px; }
- .dw-burger-panel { top: 62px; right: 12px; }
- }
- /* Avoid double-print on sites already adopting a top-of-page header
- that overlaps z-index. Reserve safe space near top-right. */
-</style>
-<button type="button" class="dw-burger-btn" id="dwBurgerBtn"
- aria-label="Toggle menu" aria-controls="dwBurgerPanel"
- aria-expanded="false">
- <span></span><span></span><span></span>
-</button>
-<nav class="dw-burger-panel" id="dwBurgerPanel" aria-label="Primary">
- <a href="/">Home</a>
- <a href="/about">About</a>
- <a href="mailto:info@bestwallpaperplace.com">Contact</a>
-</nav>
-<script>
-(function(){
- var btn = document.getElementById('dwBurgerBtn');
- var panel = document.getElementById('dwBurgerPanel');
- if (!btn || !panel) return;
- function close(){ panel.classList.remove('open'); btn.setAttribute('aria-expanded','false'); }
- function toggle(){
- var open = panel.classList.toggle('open');
- btn.setAttribute('aria-expanded', open ? 'true' : 'false');
- }
- btn.addEventListener('click', function(e){ e.stopPropagation(); toggle(); });
- document.addEventListener('click', function(e){
- if (panel.classList.contains('open') && !panel.contains(e.target) && e.target !== btn) close();
- });
- document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
-})();
-</script>
-<!-- /DW-HAMBURGER-NAV v1 -->
-
-</body>
-</html>
diff --git a/public/index.html.pre-sort-frontend b/public/index.html.pre-sort-frontend
deleted file mode 100644
index 285ec07..0000000
--- a/public/index.html.pre-sort-frontend
+++ /dev/null
@@ -1,430 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<!-- GA4 gtag (auto) -->
-<script async src="https://www.googletagmanager.com/gtag/js?id=G-SSNGG2BQC3"></script>
-<script>
- window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', 'G-SSNGG2BQC3');
-</script>
-<!-- /GA4 gtag -->
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>Best Wallpaper Place — Hand-picked, the very best wallpaper</title>
-<meta name="description" content="Best Wallpaper Place is a curated showcase of the finest wallpaper patterns — hand-picked from premium vendors. The patterns we keep coming back to.">
-<meta name="theme-color" content="#faf8f3">
-<link rel="canonical" href="https://bestwallpaperplace.com/">
-<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ctext y='20' font-size='22'%3E%E2%98%85%3C/text%3E%3C/svg%3E">
-<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
-
-<meta property="og:type" content="website">
-<meta property="og:url" content="https://bestwallpaperplace.com/">
-<meta property="og:title" content="Best Wallpaper Place — Hand-picked wallpaper favorites">
-<meta property="og:description" content="A curated showcase of the finest wallpaper patterns. The ones we keep coming back to.">
-<meta name="twitter:card" content="summary_large_image">
-
-<script>
-(function(){ try { var t = localStorage.getItem('bwp_theme') || 'light'; document.documentElement.dataset.theme = t; } catch(e){} })();
-</script>
-<style>
-:root {
- --bg: #faf8f3;
- --paper: #1a1a1a;
- --muted: #7a7a7a;
- --line: #e5e0d6;
- --accent: #b8860b;
- --bg-soft: #f0ebe0;
- --cols: 5;
-}
-html[data-theme="dark"] {
- --bg: #14110d;
- --paper: #faf8f3;
- --muted: #9a8f7a;
- --line: rgba(255,255,255,0.10);
- --accent: #d4a017;
- --bg-soft: #1f1a14;
-}
-* { margin:0; padding:0; box-sizing:border-box; }
-html { scroll-behavior:smooth; }
-body {
- font-family:'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
- color:var(--paper);
- background:var(--bg);
- -webkit-font-smoothing:antialiased;
- min-height:100vh;
-}
-h1, h2, h3 { font-family:'Cormorant Garamond', Georgia, serif; font-weight:500; letter-spacing:-0.01em; }
-
-/* Header */
-header {
- position:sticky; top:0; z-index:100;
- display:flex; justify-content:space-between; align-items:center;
- padding:18px 32px;
- background:color-mix(in srgb, var(--bg) 92%, transparent);
- backdrop-filter:blur(12px);
- border-bottom:1px solid var(--line);
-}
-.brand { display:flex; align-items:center; gap:12px; text-decoration:none; color:var(--paper); }
-.brand .star { font-size:22px; color:var(--accent); line-height:1; }
-.brand .name { font-family:'Cormorant Garamond', serif; font-size:20px; font-weight:600; letter-spacing:0.02em; }
-.brand .name sub { font-size:9px; letter-spacing:0.32em; text-transform:uppercase; color:var(--muted); display:block; margin-top:2px; font-family:'Inter',sans-serif; font-weight:600; }
-.h-actions { display:flex; align-items:center; gap:14px; }
-.theme-toggle {
- background:transparent; border:1px solid var(--line);
- width:36px; height:36px; border-radius:50%;
- cursor:pointer; color:var(--paper);
- display:inline-flex; align-items:center; justify-content:center;
- font-size:15px; transition:all .15s; padding:0;
-}
-.theme-toggle:hover { border-color:var(--accent); color:var(--accent); }
-
-/* Hero */
-.hero {
- padding:96px 32px 64px;
- text-align:center;
- border-bottom:1px solid var(--line);
-}
-.hero .eyebrow {
- font-size:11px; letter-spacing:0.4em; text-transform:uppercase;
- font-weight:700; color:var(--muted); margin-bottom:18px;
-}
-.hero h1 {
- font-size:clamp(48px, 7vw, 96px);
- line-height:1.05;
- margin:0 auto 18px;
- max-width:1100px;
-}
-.hero h1 .accent { color:var(--accent); font-style:italic; }
-.hero .sub {
- font-size:clamp(15px, 1.6vw, 19px);
- color:var(--muted); max-width:620px; margin:0 auto 32px;
- line-height:1.55;
-}
-.hero .stats {
- display:flex; justify-content:center; gap:48px; flex-wrap:wrap;
- font-size:11px; letter-spacing:0.22em; text-transform:uppercase;
- font-weight:700; color:var(--muted);
-}
-.hero .stats span b { display:block; font-family:'Cormorant Garamond',serif; font-size:28px; font-weight:500; color:var(--paper); letter-spacing:0; text-transform:none; margin-top:4px; }
-
-/* Toolbar */
-.section { padding:48px 32px 96px; max-width:1600px; margin:0 auto; }
-.toolbar {
- display:flex; align-items:center; gap:16px;
- flex-wrap:wrap;
- margin-bottom:24px; padding-bottom:18px;
- border-bottom:1px solid var(--line);
-}
-.toolbar .search {
- flex:0 1 280px; display:flex; align-items:center; gap:10px;
- border-bottom:1px solid var(--line); padding:6px 0;
-}
-.toolbar .search input {
- flex:1; background:transparent; border:0; color:var(--paper);
- font-family:inherit; font-size:13px; outline:none; letter-spacing:0.02em;
-}
-.toolbar .search input::placeholder { color:var(--muted); }
-.toolbar .ctl {
- display:flex; align-items:center; gap:10px;
- font-size:10px; letter-spacing:0.28em; text-transform:uppercase;
- font-weight:700; color:var(--muted);
-}
-.toolbar select {
- background:transparent; border:1px solid var(--line);
- padding:8px 12px; color:var(--paper);
- font-family:inherit; font-size:11px;
- letter-spacing:0.18em; text-transform:uppercase; font-weight:600;
- cursor:pointer; outline:none;
-}
-.toolbar select:hover { border-color:var(--accent); }
-.density input[type=range] {
- -webkit-appearance:none; appearance:none;
- width:140px; height:1px; background:var(--line);
- outline:none;
-}
-.density input[type=range]::-webkit-slider-thumb {
- -webkit-appearance:none; appearance:none;
- width:14px; height:14px; background:var(--accent);
- cursor:pointer; border-radius:50%;
-}
-.density input[type=range]::-moz-range-thumb {
- width:14px; height:14px; background:var(--accent);
- cursor:pointer; border-radius:50%; border:0;
-}
-.stat-line {
- font-size:10px; letter-spacing:0.32em; text-transform:uppercase;
- font-weight:600; color:var(--muted); margin-bottom:24px;
-}
-
-/* Grid */
-.grid {
- display:grid; grid-template-columns:repeat(var(--cols), 1fr);
- gap:2px;
-}
-.card {
- position:relative; aspect-ratio:1/1.15;
- cursor:pointer; overflow:hidden;
- background:var(--bg-soft);
- transition:transform 0.4s cubic-bezier(0.2,0.8,0.2,1);
-}
-.card:hover { transform:scale(1.04); z-index:5; }
-.card img {
- width:100%; height:100%; object-fit:cover; display:block;
- transition:transform 0.4s ease;
-}
-.card .overlay {
- position:absolute; left:0; right:0; bottom:0;
- padding:18px 14px 12px;
- background:linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.85) 100%);
- transform:translateY(50%); opacity:0.85;
- transition:transform 0.3s ease, opacity 0.3s ease;
-}
-.card:hover .overlay { transform:translateY(0); opacity:1; }
-.card .pat {
- font-size:13px; font-weight:600; color:#fff;
- line-height:1.25;
- display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
- overflow:hidden;
-}
-.card .ven {
- font-size:9px; letter-spacing:0.18em; text-transform:uppercase;
- color:#fff; opacity:0.7; font-weight:500; margin-top:4px;
-}
-.card .actions { margin-top:10px; display:flex; gap:6px; }
-.card .sample-btn {
- flex:1; padding:7px 10px;
- background:#fff; color:#000;
- font-family:inherit; font-size:10px; letter-spacing:0.22em;
- text-transform:uppercase; font-weight:700;
- border:0; cursor:pointer; text-align:center;
- text-decoration:none;
- transition:all 0.2s;
-}
-.card .sample-btn:hover { background:var(--accent); color:#fff; }
-
-@media (max-width:1024px) { .grid { grid-template-columns:repeat(min(var(--cols), 4), 1fr); } }
-@media (max-width:720px) {
- .grid { grid-template-columns:repeat(2, 1fr); }
- .density { display:none; }
- header { padding:14px 20px; }
- .section { padding-left:20px; padding-right:20px; }
- .hero { padding:64px 20px 48px; }
-}
-
-.loading {
- text-align:center; color:var(--muted);
- padding:32px;
- font-size:10px; letter-spacing:0.32em; text-transform:uppercase; font-weight:700;
-}
-
-/* Footer */
-footer { padding:64px 32px 32px; border-top:1px solid var(--line); margin-top:32px; }
-.footer-grid {
- max-width:1400px; margin:0 auto;
- display:grid; grid-template-columns:2fr 1fr 1fr;
- gap:48px; margin-bottom:48px;
-}
-.footer-brand { font-family:'Cormorant Garamond',serif; font-size:24px; font-weight:500; margin-bottom:12px; }
-.footer-text { font-size:13px; line-height:1.6; color:var(--muted); max-width:380px; }
-.footer-col h4 {
- font-size:10px; letter-spacing:0.32em; text-transform:uppercase;
- font-weight:700; color:var(--paper); margin-bottom:18px;
- font-family:'Inter',sans-serif;
-}
-.footer-col a, .footer-col p {
- display:block; font-size:13px; color:var(--muted);
- text-decoration:none; margin-bottom:8px; transition:color 0.2s;
-}
-.footer-col a:hover { color:var(--accent); }
-.footer-bottom {
- max-width:1400px; margin:0 auto;
- padding-top:24px; border-top:1px solid var(--line);
- display:flex; justify-content:space-between; flex-wrap:wrap; gap:16px;
- font-size:11px; letter-spacing:0.18em; text-transform:uppercase; color:var(--muted);
-}
-@media (max-width:720px) { .footer-grid { grid-template-columns:1fr; gap:32px; } }
-
-/* Focus */
-a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible {
- outline: 2px solid var(--accent); outline-offset: 2px;
-}
-</style>
-</head>
-<body>
-<header>
- <a href="/" class="brand">
- <span class="star">★</span>
- <span class="name">Best Wallpaper Place<sub>Hand-picked favorites</sub></span>
- </a>
- <div class="h-actions">
- <button class="theme-toggle" id="themeToggle" aria-label="Toggle dark mode" title="Toggle theme">☾</button>
- </div>
-</header>
-
-<section class="hero">
- <div class="eyebrow">Curated · Independent · No sponsored placements</div>
- <h1>The wallpaper we'd put on <span class="accent">our own walls.</span></h1>
- <p class="sub">A hand-picked showcase of patterns from the world's best wallcovering houses — the ones we keep coming back to. Browse, sort, request a sample.</p>
- <div class="stats">
- <span><b id="statCount">—</b>Patterns</span>
- <span><b id="statVendors">—</b>Houses</span>
- <span><b>2026</b>Updated</span>
- </div>
-</section>
-
-<section class="section">
- <div class="toolbar">
- <div class="search">
- <svg width="16" height="16" stroke="currentColor" stroke-width="1.5" fill="none" viewBox="0 0 24 24" style="color:var(--muted);"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
- <input id="searchBox" type="text" placeholder="Search patterns, colors, vendors…">
- </div>
- <div class="ctl">
- <span>Sort</span>
- <select id="sortSelect">
- <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>
- <option value="price-asc">Price ↑</option>
- <option value="price-desc">Price ↓</option>
- </select>
- </div>
- <div class="ctl density">
- <span>Density</span>
- <input id="densitySlider" type="range" min="2" max="8" value="5">
- </div>
- </div>
- <div class="stat-line" id="statLine">Loading…</div>
- <div class="grid" id="grid"></div>
- <div class="loading" id="loading" style="display:none;">Loading more…</div>
-</section>
-
-<footer>
- <div class="footer-grid">
- <div>
- <div class="footer-brand">Best Wallpaper Place</div>
- <p class="footer-text">A curated showcase of premium wallpaper from independent and design-house vendors. Memo samples available on every pattern. Sourced and curated by Designer Wallcoverings.</p>
- </div>
- <div class="footer-col">
- <h4>Contact</h4>
- <a href="mailto:info@bestwallpaperplace.com">info@bestwallpaperplace.com</a>
- <a href="tel:+18883734564">888-373-4564</a>
- </div>
- <div class="footer-col">
- <h4>Visit</h4>
- <p>Designer Wallcoverings</p>
- <p>15442 Ventura Bl #102</p>
- <p>Sherman Oaks, CA 91403</p>
- </div>
- </div>
- <div class="footer-bottom">
- <span>© 2026 Best Wallpaper Place</span>
- <span>A Designer Wallcoverings property</span>
- </div>
-</footer>
-
-<script>
-(function(){
- const grid = document.getElementById('grid');
- const statLine = document.getElementById('statLine');
- const loading = document.getElementById('loading');
- const sortSelect = document.getElementById('sortSelect');
- const searchBox = document.getElementById('searchBox');
- const density = document.getElementById('densitySlider');
- const themeBtn = document.getElementById('themeToggle');
-
- // theme
- function applyTheme(t) {
- document.documentElement.dataset.theme = t;
- themeBtn.textContent = t === 'dark' ? '☀' : '☾';
- try { localStorage.setItem('bwp_theme', t); } catch(e){}
- }
- applyTheme(localStorage.getItem('bwp_theme') || 'light');
- themeBtn.addEventListener('click', () => {
- applyTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark');
- });
-
- // density
- function applyCols(n) {
- document.documentElement.style.setProperty('--cols', n);
- try { localStorage.setItem('bwp_cols', n); } catch(e){}
- }
- const savedCols = parseInt(localStorage.getItem('bwp_cols') || '5', 10);
- density.value = savedCols;
- applyCols(savedCols);
- density.addEventListener('input', e => applyCols(e.target.value));
-
- // sort + search persistence
- const savedSort = localStorage.getItem('bwp_sort') || 'newest';
- sortSelect.value = savedSort;
- sortSelect.addEventListener('change', () => {
- try { localStorage.setItem('bwp_sort', sortSelect.value); } catch(e){}
- reset();
- });
- let searchTimer;
- searchBox.addEventListener('input', () => {
- clearTimeout(searchTimer);
- searchTimer = setTimeout(reset, 250);
- });
-
- // facets
- fetch('/api/facets').then(r => r.json()).then(d => {
- document.getElementById('statCount').textContent = d.total.toLocaleString();
- document.getElementById('statVendors').textContent = Object.keys(d.vendors || {}).length;
- }).catch(()=>{});
-
- // grid render + infinite scroll
- let page = 1, pages = 1, total = 0, busy = false;
- function reset() {
- page = 1; pages = 1; total = 0;
- grid.innerHTML = '';
- statLine.textContent = 'Loading…';
- fetchPage();
- }
- function card(p) {
- const handle = p.handle || p.sku || '';
- const ven = p.vendor || '';
- const title = (p.title || '').replace(/ \| .*$/, '');
- const img = p.image_url || '';
- const sampleUrl = '/sample/' + encodeURIComponent(handle);
- return `<article class="card">
- <a href="${sampleUrl}" target="_blank" rel="noopener" aria-label="View ${title}">
- <img loading="lazy" src="${img}" alt="${title.replace(/"/g,'"')}">
- </a>
- <div class="overlay">
- <div class="pat">${title.replace(/</g,'<')}</div>
- <div class="ven">${ven.replace(/</g,'<')}</div>
- <div class="actions">
- <a class="sample-btn" href="${sampleUrl}" target="_blank" rel="noopener">Request Sample</a>
- </div>
- </div>
- </article>`;
- }
- function fetchPage() {
- if (busy || page > pages) return;
- busy = true; loading.style.display = 'block';
- const q = encodeURIComponent(searchBox.value.trim());
- const s = encodeURIComponent(sortSelect.value);
- fetch(`/api/products?page=${page}&limit=30&q=${q}&sort=${s}`)
- .then(r => r.json())
- .then(d => {
- pages = d.pages; total = d.total;
- statLine.textContent = `${total.toLocaleString()} patterns · sort: ${d.sort}`;
- grid.insertAdjacentHTML('beforeend', d.items.map(card).join(''));
- page++;
- })
- .finally(() => { busy = false; loading.style.display = 'none'; });
- }
- // infinite scroll
- window.addEventListener('scroll', () => {
- if (busy) return;
- if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 600) fetchPage();
- });
- fetchPage();
-})();
-</script>
-</body>
-</html>
diff --git a/server.js b/server.js
index f536888..3463297 100644
--- a/server.js
+++ b/server.js
@@ -112,6 +112,15 @@ function sortProducts(list, mode) {
const app = express();
app.use(helmet({ contentSecurityPolicy: false }));
app.use(express.json({ limit: '256kb' }));
+
+// Never serve snapshot/backup files from the static root.
+app.use((req, res, next) => {
+ if (/\.(bak|pre-[\w.-]*)/i.test(req.path) || /\.pre-/i.test(req.path)) {
+ return res.status(404).send('Not found');
+ }
+ next();
+});
+
app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
app.get('/api/products', (req, res) => {
diff --git a/server.js.pre-sort-skill b/server.js.pre-sort-skill
deleted file mode 100644
index f60e46c..0000000
--- a/server.js.pre-sort-skill
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Best Wallpaper Place — curated "best of" showcase
- * Reads data/products.json (aggregated from DW family vendor catalogs).
- */
-const express = require('express');
-const helmet = require('helmet');
-const path = require('path');
-const fs = require('fs');
-
-const PORT = process.env.PORT || 9920;
-const SITE = 'bestwallpaperplace';
-const DW_SHOPIFY = 'https://designerwallcoverings.com';
-
-let DATA_RAW;
-try {
- DATA_RAW = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8'));
- 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}`);
- DATA_RAW = [];
-}
-
-function isJunk(p) {
- if (!p.image_url || !p.image_url.trim()) return true;
- if (!p.handle && !p.sku) return true;
- const t = p.title || '';
- if (/lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i.test(t)) return true;
- if (/visual.{0,3}merchandiser/i.test(t)) return true;
- if (/(?:^|\W)image[ _-]?4(?:\W|$)/i.test(t)) return true;
- return false;
-}
-
-const PRODUCTS = DATA_RAW.filter(p => !isJunk(p));
-console.log(`[${SITE}] loaded ${DATA_RAW.length}, kept ${PRODUCTS.length}`);
-
-const COLOR_RANK = {
- black:0,gray:1,grey:1,silver:2,white:3,ivory:4,cream:5,beige:6,brown:7,tan:8,khaki:9,copper:10,
- red:20,pink:21,coral:22,orange:23,peach:24,salmon:25,
- yellow:30,gold:31,mustard:32,olive:33,
- green:40,lime:41,mint:42,teal:50,aqua:51,turquoise:52,cyan:53,
- blue:60,navy:61,indigo:62,periwinkle:63,
- purple:70,violet:71,lavender:72,plum:73,magenta:74,fuchsia:75,
-};
-function colorRank(p) {
- const tags = (p.tags || []).map(t => String(t).toLowerCase());
- for (const t of tags) for (const k of Object.keys(COLOR_RANK)) if (t.includes(k)) return COLOR_RANK[k];
- return 999;
-}
-const STYLE_TAGS = ['traditional','transitional','modern','contemporary','minimalist','art deco','victorian','mid-century','rustic','industrial','farmhouse','bohemian','geometric','floral','botanical','damask','toile','grasscloth','silk','linen','vinyl','natural'];
-function styleKey(p) {
- const tags = (p.tags || []).map(t => String(t).toLowerCase());
- for (const s of STYLE_TAGS) if (tags.some(t => t.includes(s))) return s;
- return 'zzz';
-}
-function sortProducts(list, mode) {
- if (mode === 'sku') return [...list].sort((a,b) => String(a.sku || a.handle || '').localeCompare(String(b.sku || b.handle || '')));
- if (mode === 'title') return [...list].sort((a,b) => String(a.title || '').localeCompare(String(b.title || '')));
- if (mode === 'color') return [...list].sort((a,b) => colorRank(a) - colorRank(b) || String(a.title || '').localeCompare(String(b.title || '')));
- if (mode === 'style') return [...list].sort((a,b) => styleKey(a).localeCompare(styleKey(b)) || String(a.title || '').localeCompare(String(b.title || '')));
- if (mode === 'price-asc') return [...list].sort((a,b) => (Number(a.max_price) || 0) - (Number(b.max_price) || 0));
- if (mode === 'price-desc') return [...list].sort((a,b) => (Number(b.max_price) || 0) - (Number(a.max_price) || 0));
- return list; // 'newest' — keep as-loaded order (already sorted by tag-richness)
-}
-
-const app = express();
-app.use(helmet({ contentSecurityPolicy: false }));
-app.use(express.json({ limit: '256kb' }));
-app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
-
-app.get('/api/products', (req, res) => {
- const { q, vendor, sort = 'newest', page = 1, limit = 24 } = req.query;
- let list = PRODUCTS;
- if (q) {
- const needle = String(q).toLowerCase();
- list = list.filter(p =>
- (p.title || '').toLowerCase().includes(needle) ||
- (p.tags || []).some(t => String(t).toLowerCase().includes(needle))
- );
- }
- if (vendor && vendor !== 'all') list = list.filter(p => p.vendor === vendor);
- list = sortProducts(list, sort);
- const total = list.length;
- const pageNum = Math.max(1, parseInt(page) || 1);
- const lim = Math.min(60, parseInt(limit) || 24);
- const start = (pageNum - 1) * lim;
- res.json({ total, page: pageNum, limit: lim, pages: Math.ceil(total / lim), sort, items: list.slice(start, start + lim) });
-});
-
-app.get('/api/facets', (req, res) => {
- const vendors = {};
- for (const p of PRODUCTS) vendors[p.vendor || 'Unknown'] = (vendors[p.vendor || 'Unknown'] || 0) + 1;
- res.json({ vendors, total: PRODUCTS.length });
-});
-
-app.get('/api/health', (req, res) => res.json({ status: 'ok', count: PRODUCTS.length }));
-
-app.get('/sample/:handle', (req, res) => {
- const p = PRODUCTS.find(x => x.handle === req.params.handle || x.sku === req.params.handle);
- if (!p) return res.status(404).send('Not found');
- res.redirect(302, p.product_url || `${DW_SHOPIFY}/products/${encodeURIComponent(p.handle)}#sample`);
-});
-
-app.get('/robots.txt', (req, res) => {
- res.type('text/plain').send(`User-agent: *
-Allow: /
-Sitemap: https://bestwallpaperplace.com/sitemap.xml
-`);
-});
-app.get('/sitemap.xml', (req, res) => {
- const xml = `<?xml version="1.0" encoding="UTF-8"?>
-<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
- <url><loc>https://bestwallpaperplace.com/</loc><changefreq>weekly</changefreq></url>
-</urlset>`;
- res.type('application/xml').send(xml);
-});
-
-app.listen(PORT, '127.0.0.1', () => {
- console.log(`[${SITE}] listening on http://127.0.0.1:${PORT}`);
-});
← 1a30791 snapshot before refactor pass
·
back to Bestwallpaperplace
·
Fix dead /about nav link and add noreferrer to external link 41b7017 →