← back to Interiordesignershowroom
nav: promote kitchen/bathroom to rooms + upper-right room hamburger (collapsed on load, all room types)
ecc8fe048bb7fc20bf145e79d00dad3612ff1d3e · 2026-08-03 09:59:41 -0700 · Steve Abrams
Files touched
A lib/nav.jsM lib/render.jsM public/css/site.cssA public/js/roommenu.jsM server.js
Diff
commit ecc8fe048bb7fc20bf145e79d00dad3612ff1d3e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 09:59:41 2026 -0700
nav: promote kitchen/bathroom to rooms + upper-right room hamburger (collapsed on load, all room types)
---
lib/nav.js | 20 ++++++++++++++++++++
lib/render.js | 22 ++++++++++++++++++++++
public/css/site.css | 47 +++++++++++++++++++++++++++++++++++++++++++++++
public/js/roommenu.js | 18 ++++++++++++++++++
server.js | 5 +----
5 files changed, 108 insertions(+), 4 deletions(-)
diff --git a/lib/nav.js b/lib/nav.js
new file mode 100644
index 0000000..7248447
--- /dev/null
+++ b/lib/nav.js
@@ -0,0 +1,20 @@
+// Canonical list of browsable room categories — the ONE source of truth shared by
+// server.js (the /rooms/:room route whitelist, ROOM_SLUGS, and sitemap) and
+// render.js (the header hamburger). Keeping it here means the nav menu, the routes
+// that 200 vs 404, and the sitemap can never drift out of sync.
+//
+// [slug, Label]. A slug earns a spot here only if it's a real category with its own
+// nav page; DB-artifact room values (e.g. "paint") are deliberately excluded.
+const ROOMS = [
+ ['living-room', 'Living Room'],
+ ['bedroom', 'Bedroom'],
+ ['dining', 'Dining'],
+ ['kitchen', 'Kitchen'],
+ ['bathroom', 'Bathroom'],
+ ['office', 'Office'],
+ ['lighting', 'Lighting'],
+ ['decor', 'Decor'],
+ ['outdoor', 'Outdoor'],
+];
+
+module.exports = { ROOMS };
diff --git a/lib/render.js b/lib/render.js
index d6a5138..8c2276c 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -2,6 +2,8 @@
// affiliate sites live or die on SEO, and crawlers reward real HTML with proper
// <title>/meta/canonical/JSON-LD over JS-hydrated shells.
+const { ROOMS } = require('./nav'); // shared room list — powers the header hamburger
+
const SITE = {
name: 'Interior Designer\'s Showroom',
tagline: 'A curated showroom of designer-worthy pieces — shop the look, room by room.',
@@ -166,6 +168,24 @@ function layout({ title, description, canonical, jsonld, image, body, activeNav
].map(([href, label]) =>
`<a href="${href}"${activeNav === href ? ' class="active" aria-current="page"' : ''}>${label}</a>`).join('');
+ // Upper-right hamburger of ALL room types. Native <details> => collapsed on load
+ // with zero JS, focusable + keyboard-toggleable (Enter/Space on the summary),
+ // and it closes on outside-click / Escape via a tiny enhancement in moodboard.js.
+ const roomLinks = ROOMS.map(([slug, label]) => {
+ const href = `/rooms/${slug}`;
+ return `<a role="menuitem" href="${href}"${activeNav === href ? ' aria-current="page"' : ''}>${esc(label)}</a>`;
+ }).join('');
+ const roomMenu = `<details class="room-menu" id="roomMenu">
+ <summary class="room-menu__btn" aria-label="Browse room categories" title="Shop by room">
+ <span class="hamburger" aria-hidden="true"></span>
+ <span class="room-menu__label">Rooms</span>
+ </summary>
+ <div class="room-menu__panel" role="menu" aria-label="Shop by room">
+ <p class="room-menu__heading">Shop by Room</p>
+ ${roomLinks}
+ </div>
+ </details>`;
+
return `<!doctype html>
<html lang="en">
<head>
@@ -198,6 +218,7 @@ ${disclosureBar()}
<span class="brand-word">${esc(SITE.name)}</span>
</a>
<nav class="site-nav" aria-label="Main navigation">${nav}</nav>
+ ${roomMenu}
</header>
<main id="main-content">${body}</main>
<footer class="site-footer">
@@ -213,6 +234,7 @@ ${disclosureBar()}
<p class="fine">© ${new Date().getFullYear()} ${esc(SITE.name)}. Prices and availability are accurate as of the date/time shown and are subject to change. As an Amazon Associate we earn from qualifying purchases.</p>
</footer>
<script src="/js/moodboard.js" defer></script>
+<script src="/js/roommenu.js" defer></script>
<script src="/js/adminbar.js" defer></script>
</body>
</html>`;
diff --git a/public/css/site.css b/public/css/site.css
index ccd659f..e875d72 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -1741,3 +1741,50 @@ body.admin-view .card{outline:2px solid #e6c98a;outline-offset:-2px}
.admin-chip .ac-feat{background:#fff;border:1px solid #d9d1c4;border-radius:5px;padding:3px 9px;font-size:12px;cursor:pointer;color:#6b6259}
.admin-chip .ac-feat.is-on{background:#221e19;color:#e6c98a;border-color:#221e19}
.admin-chip .ac-feat:hover{border-color:#8a7250}
+
+/* ── Header room hamburger (upper-right, collapsed on load) ──────────────── */
+.room-menu { position: relative; flex-shrink: 0; }
+.room-menu__btn {
+ list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 9px;
+ padding: 8px 13px; border: 1px solid var(--line); border-radius: 9px; background: var(--card, #fff);
+ font-size: .72rem; letter-spacing: .07em; text-transform: uppercase; color: var(--ink);
+ user-select: none; transition: border-color .15s ease, background .15s ease;
+}
+.room-menu__btn::-webkit-details-marker { display: none; }
+.room-menu__btn::marker { content: ''; }
+.room-menu__btn:hover { border-color: var(--accent); }
+.room-menu__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
+.room-menu__label { font-weight: 500; }
+/* three-bar icon → morphs to an X when open */
+.hamburger, .hamburger::before, .hamburger::after {
+ display: block; width: 18px; height: 2px; background: currentColor; border-radius: 2px;
+ transition: transform .2s ease, background .2s ease;
+}
+.hamburger { position: relative; }
+.hamburger::before, .hamburger::after { content: ''; position: absolute; left: 0; }
+.hamburger::before { top: -6px; }
+.hamburger::after { top: 6px; }
+.room-menu[open] .hamburger { background: transparent; }
+.room-menu[open] .hamburger::before { transform: translateY(6px) rotate(45deg); }
+.room-menu[open] .hamburger::after { transform: translateY(-6px) rotate(-45deg); }
+/* dropdown panel — overlays (absolute), does not push header content */
+.room-menu__panel {
+ position: absolute; top: calc(100% + 10px); right: 0; z-index: 200;
+ min-width: 232px; padding: 10px; background: #fff;
+ border: 1px solid var(--line); border-radius: 12px;
+ box-shadow: 0 14px 36px rgba(34, 30, 25, .16);
+ display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
+ animation: roomMenuIn .16s ease;
+}
+@keyframes roomMenuIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
+.room-menu__heading {
+ grid-column: 1 / -1; margin: 2px 6px 6px; font-size: .66rem; letter-spacing: .09em;
+ text-transform: uppercase; color: var(--muted, #8a8178);
+}
+.room-menu__panel a {
+ padding: 9px 10px; border-radius: 8px; font-size: .85rem; color: var(--ink);
+ text-decoration: none; transition: background .12s ease, color .12s ease;
+}
+.room-menu__panel a:hover { background: var(--bg, #f4efe8); color: var(--accent); }
+.room-menu__panel a[aria-current="page"] { background: var(--ink); color: #fff; }
+@media (max-width: 520px) { .room-menu__label { display: none; } .room-menu__panel { position: fixed; left: 12px; right: 12px; min-width: 0; } }
diff --git a/public/js/roommenu.js b/public/js/roommenu.js
new file mode 100644
index 0000000..1da2a2b
--- /dev/null
+++ b/public/js/roommenu.js
@@ -0,0 +1,18 @@
+// Progressive enhancement for the header room hamburger. The menu is a native
+// <details> (collapsed on load, works with zero JS); this only adds the niceties
+// users expect from a dropdown: close on outside-click and on Escape. If this file
+// fails to load, the menu still opens/closes via the summary button.
+(function () {
+ var menu = document.getElementById('roomMenu');
+ if (!menu) return;
+ document.addEventListener('click', function (e) {
+ if (menu.open && !menu.contains(e.target)) menu.open = false;
+ });
+ document.addEventListener('keydown', function (e) {
+ if (e.key === 'Escape' && menu.open) {
+ menu.open = false;
+ var s = menu.querySelector('summary');
+ if (s) s.focus();
+ }
+ });
+})();
diff --git a/server.js b/server.js
index 0dff063..dbe5e1b 100644
--- a/server.js
+++ b/server.js
@@ -18,10 +18,7 @@ app.use('/css', express.static(path.join(__dirname, 'public/css')));
app.use('/js', express.static(path.join(__dirname, 'public/js')));
app.use('/img', express.static(path.join(__dirname, 'public/img')));
-const ROOMS = [
- ['living-room', 'Living Room'], ['bedroom', 'Bedroom'], ['dining', 'Dining'],
- ['office', 'Office'], ['lighting', 'Lighting'], ['decor', 'Decor'], ['outdoor', 'Outdoor'],
-];
+const { ROOMS } = require('./lib/nav'); // shared with render.js (nav) — one source of truth
// --- tiny markdown-lite for guide bodies (headings, bold, paragraphs) ------
function md(src = '') {
← 6a15502 seo: indexable facet landing pages — sitemap + self-canonica
·
back to Interiordesignershowroom
·
seo: breadcrumbs (nav + BreadcrumbList) across facet/room/gu 5b5102f →