← back to Rentv 2026
Build missing topic.html + topics.html — fix broken /topic/* and /topics routes (TK-10282)
939f491c7588747e60d91fdd4d2c123cf558f42a · 2026-08-06 08:24:09 -0700 · Steve Abrams
The routes existed in server.js but the page shells were missing, so /topic/:slug and /topics 404'd via sendPage's missing-file guard — which also broke the section→hub 301 chain (/financing → /topic/... → 404).
- public/topic.html: single-hub landing page (homepage drawer chrome + auto-injected tab strip), fetches /api/hub/:slug → h1/dek/siblings/articles, JSON-LD, noindex-when-empty
- public/topics.html: hub index grouped by /api/hubs groups, indexable hubs only
Verified locally: /topics 200 (7 hubs/5 groups), /topic/office 200 (5 stories, 6 siblings), /financing 301 → /topic/capital-markets → 200. No rentv.com links.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A public/topic.htmlA public/topics.html
Diff
commit 939f491c7588747e60d91fdd4d2c123cf558f42a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 6 08:24:09 2026 -0700
Build missing topic.html + topics.html — fix broken /topic/* and /topics routes (TK-10282)
The routes existed in server.js but the page shells were missing, so /topic/:slug and /topics 404'd via sendPage's missing-file guard — which also broke the section→hub 301 chain (/financing → /topic/... → 404).
- public/topic.html: single-hub landing page (homepage drawer chrome + auto-injected tab strip), fetches /api/hub/:slug → h1/dek/siblings/articles, JSON-LD, noindex-when-empty
- public/topics.html: hub index grouped by /api/hubs groups, indexable hubs only
Verified locally: /topics 200 (7 hubs/5 groups), /topic/office 200 (5 stories, 6 siblings), /financing 301 → /topic/capital-markets → 200. No rentv.com links.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/topic.html | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++
public/topics.html | 112 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 240 insertions(+)
diff --git a/public/topic.html b/public/topic.html
new file mode 100644
index 00000000..f120d106
--- /dev/null
+++ b/public/topic.html
@@ -0,0 +1,128 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>RENTV.com — Topic</title>
+<meta name="description" content="Commercial real estate news by topic — RENTV.com, Western U.S.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--wash:#f5f6f8}
+ *{box-sizing:border-box;margin:0}
+ body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:#fbfbfc;line-height:1.45}
+ a{color:inherit;text-decoration:none}
+ .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}
+ .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:inherit;padding:7px 9px;border-radius:8px}
+ .burger:hover{background:rgba(0,0,0,.06)}
+ .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sub)}
+ .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em}.brand span{color:var(--red)}
+ .user{width:37px;height:37px;border-radius:50%;border:1.5px solid var(--line);background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ink)}
+ .user:hover{border-color:var(--red);color:var(--red)}
+ .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+ .scrim.open{opacity:1;pointer-events:auto}
+ .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+ .drawer.open{transform:none}
+ .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+ .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800}.drawer .dh .w span{color:var(--red)}
+ .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+ .grp{padding:14px 18px 4px}.grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+ .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+ .drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
+ .drawer nav a.active{background:#141a21;border-left-color:var(--red);color:#fff;font-weight:700}
+ .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+ .wrap{max-width:1240px;margin:0 auto;padding:0 22px;width:100%}
+ .crumb{font-size:12px;color:var(--sub);padding:18px 0 0}.crumb a{color:var(--red);font-weight:700}
+ .head{padding:16px 0 6px;border-bottom:1px solid #e8e4e0;margin-bottom:6px}
+ .head .chip{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.08em;color:var(--red);margin-bottom:6px}
+ .head h1{font-family:'Playfair Display',serif;font-size:clamp(30px,3.4vw,46px);font-weight:700;letter-spacing:-.01em;line-height:1.06}
+ .head .dek{color:var(--sub);font-size:14.5px;max-width:70ch;margin-top:10px}
+ .subs{display:flex;flex-wrap:wrap;gap:8px;padding:16px 0 4px}
+ .subs a{font-size:12.5px;font-weight:700;border:1px solid var(--line);background:#fff;border-radius:20px;padding:6px 13px;color:#2b3138}
+ .subs a:hover{border-color:var(--red);color:var(--red)}
+ .live{display:inline-flex;align-items:center;gap:7px;font-size:11px;color:var(--sub);letter-spacing:.06em;text-transform:uppercase;padding:14px 0 0}
+ .live .dot{width:7px;height:7px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.6)}70%{box-shadow:0 0 0 7px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
+ .river{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;padding:20px 0 40px}
+ .river a{display:block}
+ .river .im{height:130px;border-radius:5px;background:#12161a center/cover;display:block;margin-bottom:8px}
+ .river h4{font:600 15px/1.28 'Inter';margin-top:3px}
+ .empty{padding:50px 0;color:var(--sub)}.empty a{color:var(--red);font-weight:800}
+ .skl{background:linear-gradient(90deg,#eee,#f6f6f6,#eee);background-size:200% 100%;animation:sh 1.2s infinite}@keyframes sh{to{background-position:-200% 0}}
+ footer{background:#0b0e12;color:#8b939b;padding:30px 0;font-size:12px;margin-top:20px}
+ footer .in{max-width:1240px;margin:0 auto;padding:0 22px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px}
+ footer .logo{font-family:'Playfair Display',serif;font-size:20px;font-weight:800;color:#fff}footer .logo span{color:var(--red)}
+ @media(max-width:1000px){.river{grid-template-columns:1fr 1fr}}
+ @media(max-width:560px){.river{grid-template-columns:1fr}}
+</style></head>
+<body>
+<header class="top">
+ <button class="burger" id="burger" aria-label="Menu"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg><span class="lbl">Menu</span></button>
+ <a class="brand" href="/">REN<span>TV</span></a>
+ <button class="user" id="user" title="Sign in" aria-label="Sign in"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="3.5"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0" stroke-linecap="round"/></svg></button>
+</header>
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+ <div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
+ <div class="grp"><div class="gt">Discover</div></div>
+ <nav><a href="/topics" style="font-weight:700">✨ All Topics</a><a href="/search">🔎 Search</a><a href="/market-pulse">📈 Rates & Capital Markets</a><a href="/metros">📍 Deal Flow by Metro</a><a href="/directory">🏢 CRE Directory</a><a href="/brief">📰 The RENTV Brief</a></nav>
+ <div class="grp"><div class="gt">Sections</div></div>
+ <nav><a href="/closings">Closings</a><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
+ <div class="grp"><div class="gt">Intelligence</div></div>
+ <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+ <div class="df">RENTV.com — Commercial Real Estate News across the Western U.S. since 1998.</div>
+</aside>
+
+<div class="wrap">
+ <div class="crumb"><a href="/">Home</a> › <a href="/topics">Topics</a> › <span id="crumb">Topic</span></div>
+ <div class="head">
+ <span class="chip" id="chip">Topic · Western U.S.</span>
+ <h1 id="h1">Loading…</h1>
+ <p class="dek" id="dek"></p>
+ <div class="subs" id="subs"></div>
+ <div class="live"><span class="dot"></span><span id="count">live coverage</span></div>
+ </div>
+ <div class="river" id="river">
+ <a><span class="im skl"></span><h4 class="skl"> </h4></a>
+ <a><span class="im skl"></span><h4 class="skl"> </h4></a>
+ <a><span class="im skl"></span><h4 class="skl"> </h4></a>
+ <a><span class="im skl"></span><h4 class="skl"> </h4></a>
+ </div>
+</div>
+
+<footer><div class="in">
+ <div class="logo">REN<span>TV</span>.com</div>
+ <div>© 2026 RENTV.com · Commercial real estate news, Western U.S. since 1998</div>
+</div></footer>
+
+<script>
+const $=id=>document.getElementById(id), esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+$('burger').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
+const closeDrawer=()=>{$('scrim').classList.remove('open');$('drawer').classList.remove('open');};
+$('dclose').onclick=closeDrawer;$('scrim').onclick=closeDrawer;$('user').onclick=()=>{location.href='/login';};
+const slug=(location.pathname.split('/').filter(Boolean)[1]||'').toLowerCase();
+function noindex(){let m=document.querySelector('meta[name="robots"]');if(!m){m=document.createElement('meta');m.name='robots';document.head.appendChild(m);}m.content='noindex,follow';}
+async function load(){
+ try{
+ const d=await (await fetch('/api/hub/'+encodeURIComponent(slug))).json();
+ if(d.error){$('h1').textContent='Topic not found';$('river').className='';$('river').innerHTML='<p class="empty">Unknown topic. <a href="/topics">Browse all topics →</a></p>';noindex();return;}
+ document.title='RENTV.com — '+(d.h1||d.title);
+ document.querySelector('meta[name=description]').content=d.dek||('Commercial real estate news — '+d.title+'.');
+ $('crumb').textContent=d.title;$('h1').textContent=d.h1||d.title;$('dek').textContent=d.dek||'';
+ $('chip').textContent='Topic · Western U.S.';
+ const sib=d.siblings||[];
+ $('subs').innerHTML=sib.map(s=>'<a href="/topic/'+s.slug+'">'+esc(s.title)+'</a>').join('')+'<a href="/topics">← All topics</a>';
+ const arts=d.articles||[];
+ if(!d.indexable) noindex();
+ if(!arts.length){$('river').className='';$('river').innerHTML='<p class="empty">No '+esc(d.title)+' stories in the live feed right now. <a href="/">Back to the homepage →</a></p>';$('count').textContent='0 live stories';return;}
+ $('count').textContent=arts.length+' live '+(arts.length===1?'story':'stories');
+ $('river').className='river';
+ $('river').innerHTML=arts.map(x=>'<a href="/news/'+encodeURIComponent(x.id)+'"><span class="im"'+((x.image)?(' style="background:#12161a url(\''+esc(x.image)+'\') center/cover"'):'')+'></span><h4>'+esc(x.title)+'</h4></a>').join('');
+ const ld={"@context":"https://schema.org","@type":"CollectionPage","name":d.h1||d.title,"description":d.dek,
+ "mainEntity":{"@type":"ItemList","itemListElement":arts.slice(0,20).map((x,i)=>({"@type":"ListItem","position":i+1,"url":location.origin+'/news/'+x.id,"name":x.title}))}};
+ const s=document.createElement('script');s.type='application/ld+json';s.textContent=JSON.stringify(ld);document.head.appendChild(s);
+ }catch(e){
+ $('river').className='';$('river').innerHTML='<p class="empty">Live feed temporarily unavailable — <a href="/">back to homepage</a>.</p>';
+ }
+}
+load();
+</script>
+<script src="/theme.js"></script>
+</body></html>
diff --git a/public/topics.html b/public/topics.html
new file mode 100644
index 00000000..9fa78c75
--- /dev/null
+++ b/public/topics.html
@@ -0,0 +1,112 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>RENTV.com — All Topics</title>
+<meta name="description" content="Browse RENTV commercial real estate news by topic — property types, transactions, capital markets and regions across the Western U.S.">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+ :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--wash:#f5f6f8}
+ *{box-sizing:border-box;margin:0}
+ body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:#fbfbfc;line-height:1.45}
+ a{color:inherit;text-decoration:none}
+ .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}
+ .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:inherit;padding:7px 9px;border-radius:8px}
+ .burger:hover{background:rgba(0,0,0,.06)}
+ .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sub)}
+ .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em}.brand span{color:var(--red)}
+ .user{width:37px;height:37px;border-radius:50%;border:1.5px solid var(--line);background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ink)}
+ .user:hover{border-color:var(--red);color:var(--red)}
+ .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+ .scrim.open{opacity:1;pointer-events:auto}
+ .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+ .drawer.open{transform:none}
+ .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+ .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800}.drawer .dh .w span{color:var(--red)}
+ .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+ .grp{padding:14px 18px 4px}.grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+ .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+ .drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
+ .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+ .wrap{max-width:1240px;margin:0 auto;padding:0 22px;width:100%}
+ .crumb{font-size:12px;color:var(--sub);padding:18px 0 0}.crumb a{color:var(--red);font-weight:700}
+ .head{padding:16px 0 14px;border-bottom:1px solid #e8e4e0;margin-bottom:10px}
+ .head h1{font-family:'Playfair Display',serif;font-size:clamp(30px,3.4vw,44px);font-weight:700;letter-spacing:-.01em}
+ .head .dek{color:var(--sub);font-size:14.5px;max-width:70ch;margin-top:8px}
+ .tgroup{padding:20px 0 6px}
+ .tgroup h2{font-size:13px;text-transform:uppercase;letter-spacing:.09em;border-top:2px solid var(--ink);padding-top:10px;margin-bottom:14px;font-weight:800}
+ .cards{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;padding-bottom:14px}
+ .card{display:block;border:1px solid var(--line);border-radius:10px;padding:16px 16px 14px;background:#fff;transition:border-color .14s,box-shadow .14s}
+ .card:hover{border-color:var(--red);box-shadow:0 6px 20px rgba(0,0,0,.06)}
+ .card .t{font-family:'Playfair Display',serif;font-size:19px;font-weight:700}
+ .card .d{color:var(--sub);font-size:12.5px;margin-top:5px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
+ .card .c{margin-top:10px;font-size:10px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:var(--red)}
+ .empty{padding:50px 0;color:var(--sub)}.empty a{color:var(--red);font-weight:800}
+ footer{background:#0b0e12;color:#8b939b;padding:30px 0;font-size:12px;margin-top:24px}
+ footer .in{max-width:1240px;margin:0 auto;padding:0 22px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px}
+ footer .logo{font-family:'Playfair Display',serif;font-size:20px;font-weight:800;color:#fff}footer .logo span{color:var(--red)}
+ @media(max-width:900px){.cards{grid-template-columns:1fr 1fr}}
+ @media(max-width:560px){.cards{grid-template-columns:1fr}}
+</style></head>
+<body>
+<header class="top">
+ <button class="burger" id="burger" aria-label="Menu"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg><span class="lbl">Menu</span></button>
+ <a class="brand" href="/">REN<span>TV</span></a>
+ <button class="user" id="user" title="Sign in" aria-label="Sign in"><svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="3.5"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0" stroke-linecap="round"/></svg></button>
+</header>
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+ <div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
+ <div class="grp"><div class="gt">Discover</div></div>
+ <nav><a href="/topics" style="font-weight:700">✨ All Topics</a><a href="/search">🔎 Search</a><a href="/market-pulse">📈 Rates & Capital Markets</a><a href="/metros">📍 Deal Flow by Metro</a><a href="/directory">🏢 CRE Directory</a><a href="/brief">📰 The RENTV Brief</a></nav>
+ <div class="grp"><div class="gt">Sections</div></div>
+ <nav><a href="/closings">Closings</a><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a></nav>
+ <div class="grp"><div class="gt">Intelligence</div></div>
+ <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+ <div class="df">RENTV.com — Commercial Real Estate News across the Western U.S. since 1998.</div>
+</aside>
+
+<div class="wrap">
+ <div class="crumb"><a href="/">Home</a> › <span>Topics</span></div>
+ <div class="head">
+ <h1>Browse by Topic</h1>
+ <p class="dek">Commercial real estate coverage organized by property type, transaction, participant and market — Western U.S.</p>
+ </div>
+ <div id="groups"><p class="empty">Loading topics…</p></div>
+</div>
+
+<footer><div class="in">
+ <div class="logo">REN<span>TV</span>.com</div>
+ <div>© 2026 RENTV.com · Commercial real estate news, Western U.S. since 1998</div>
+</div></footer>
+
+<script>
+const $=id=>document.getElementById(id), esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+$('burger').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
+const closeDrawer=()=>{$('scrim').classList.remove('open');$('drawer').classList.remove('open');};
+$('dclose').onclick=closeDrawer;$('scrim').onclick=closeDrawer;$('user').onclick=()=>{location.href='/login';};
+async function load(){
+ try{
+ const d=await (await fetch('/api/hubs')).json();
+ const groups=d.groups||[], hubs=(d.hubs||[]).filter(h=>h.indexable);
+ if(!hubs.length){$('groups').innerHTML='<p class="empty">No topics with live coverage yet. <a href="/">Back to the homepage →</a></p>';return;}
+ let html='';
+ groups.forEach(g=>{
+ const gh=hubs.filter(h=>h.group===g.id);
+ if(!gh.length)return;
+ html+='<section class="tgroup"><h2>'+esc(g.label)+'</h2><div class="cards">'+
+ gh.map(h=>'<a class="card" href="/topic/'+encodeURIComponent(h.slug)+'"><div class="t">'+esc(h.title)+'</div><div class="d">'+esc(h.dek||'')+'</div><div class="c">'+h.count+' '+(h.count===1?'story':'stories')+' →</div></a>').join('')+
+ '</div></section>';
+ });
+ // any indexable hub whose group isn't in groups[] (defensive)
+ const known=new Set(groups.map(g=>g.id));
+ const orphan=hubs.filter(h=>!known.has(h.group));
+ if(orphan.length){html+='<section class="tgroup"><h2>More</h2><div class="cards">'+orphan.map(h=>'<a class="card" href="/topic/'+encodeURIComponent(h.slug)+'"><div class="t">'+esc(h.title)+'</div><div class="d">'+esc(h.dek||'')+'</div><div class="c">'+h.count+' stories →</div></a>').join('')+'</div></section>';}
+ $('groups').innerHTML=html;
+ }catch(e){
+ $('groups').innerHTML='<p class="empty">Topics temporarily unavailable — <a href="/">back to homepage</a>.</p>';
+ }
+}
+load();
+</script>
+<script src="/theme.js"></script>
+</body></html>
← 0ece69ca auto-save: 2026-08-06T08:19:59 (6 files) — data/deals-regist
·
back to Rentv 2026
·
harden(deals-admin): Cody gate — every card now shows a crea 5de160cc →