[object Object]

← back to Rentv

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

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 &amp; 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">&nbsp;</h4></a>
+    <a><span class="im skl"></span><h4 class="skl">&nbsp;</h4></a>
+    <a><span class="im skl"></span><h4 class="skl">&nbsp;</h4></a>
+    <a><span class="im skl"></span><h4 class="skl">&nbsp;</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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 &amp; 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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  ·  harden(deals-admin): Cody gate — every card now shows a crea 5de160cc →