[object Object]

← back to Rentv

add in-shell article.html + section.html reader templates; wire homepage to /news/:id; robust title fallback via /api/news

d9ec1da549ac5fc03a9f3841a10be9922e0c1390 · 2026-07-20 14:20:12 -0700 · Steve Abrams

Files touched

Diff

commit d9ec1da549ac5fc03a9f3841a10be9922e0c1390
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 20 14:20:12 2026 -0700

    add in-shell article.html + section.html reader templates; wire homepage to /news/:id; robust title fallback via /api/news
---
 public/article.html | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 public/index.html   |   6 +--
 public/section.html | 116 ++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 243 insertions(+), 3 deletions(-)

diff --git a/public/article.html b/public/article.html
new file mode 100644
index 00000000..2a1d3dcf
--- /dev/null
+++ b/public/article.html
@@ -0,0 +1,124 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>RENTV.com — Story</title>
+<meta name="description" content="Commercial real estate news story — RENTV.com.">
+<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,400;0,700;1,400&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
+<style>
+  :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--sans:'Inter',system-ui,sans-serif;--serif:'Playfair Display',Georgia,serif}
+  *{box-sizing:border-box;margin:0;padding:0}
+  body{font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.6}
+  a{color:inherit;text-decoration:none}
+  .wrap{max-width:1200px;margin:0 auto;padding:0 20px}
+  .util{background:var(--ink);color:#cfd4d9;font-size:12px}
+  .util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}
+  .util .acts a{margin-left:14px;color:#cfd4d9}.util .acts a.sub{color:#fff;background:var(--red);padding:4px 12px;border-radius:3px;font-weight:700}
+  .mast{border-bottom:2px solid var(--ink);padding:18px 0 12px}
+  .mast .wrap{display:flex;align-items:center;justify-content:space-between}
+  .logo{font-family:var(--serif);font-weight:700;font-size:34px;letter-spacing:-1px}.logo span{color:var(--red)}
+  .logo small{display:block;font-size:11px;font-weight:600;letter-spacing:3px;color:var(--sub);text-transform:uppercase;margin-top:2px;font-family:var(--sans)}
+  .datebox{font-size:12px;color:var(--sub);text-align:right}
+  nav{border-bottom:1px solid var(--line);position:sticky;top:0;background:#fff;z-index:10}
+  nav .wrap{display:flex;gap:2px;align-items:center;height:46px;overflow-x:auto}
+  nav a{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:14px 13px;border-bottom:3px solid transparent;white-space:nowrap}
+  nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
+  .art{max-width:760px;margin:0 auto;padding:26px 0 10px}
+  .crumb{font-size:12px;color:var(--sub);margin-bottom:16px}.crumb a{color:var(--red);font-weight:700}
+  .chip{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--red);margin-bottom:8px}
+  h1{font-family:var(--serif);font-size:40px;line-height:1.12;letter-spacing:-.5px;margin-bottom:10px}
+  .byl{font-size:13px;color:var(--sub);border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:20px}
+  .hero{aspect-ratio:16/8;border-radius:8px;background:linear-gradient(135deg,#1f2937,#c8102e);margin:0 0 22px;overflow:hidden}
+  .hero img{width:100%;height:100%;object-fit:cover;display:block}
+  .body p{font-size:18px;margin-bottom:18px}
+  .body p:first-of-type::first-letter{font-family:var(--serif);font-size:56px;font-weight:700;float:left;line-height:.8;padding:6px 10px 0 0;color:var(--red)}
+  .src{margin:26px 0;padding:18px;background:var(--wash);border-radius:8px;font-size:14px}
+  .src a{color:var(--red);font-weight:800}
+  .back{display:inline-block;margin:10px 0 0;font-size:13px;font-weight:700;color:var(--red)}
+  .skl{background:linear-gradient(90deg,#eee,#f6f6f6,#eee);background-size:200% 100%;animation:sh 1.2s infinite;border-radius:4px;color:transparent}
+  @keyframes sh{to{background-position:-200% 0}}
+  .err{color:var(--sub);padding:20px 0}.err a{color:var(--red);font-weight:700}
+  footer{background:var(--ink);color:#aeb6bd;margin-top:36px;padding:40px 0 30px;font-size:13px}
+  footer .cols{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:28px}
+  footer .logo{color:#fff;font-size:26px;margin-bottom:8px}footer h5{color:#fff;font-size:12px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}footer a{display:block;padding:4px 0;color:#aeb6bd}
+  footer .fine{border-top:1px solid #2a2f34;margin-top:26px;padding-top:16px;font-size:12px;color:#7c848b}
+  @media(max-width:860px){h1{font-size:30px}.body p{font-size:17px}}
+</style>
+</head>
+<body>
+  <div class="util"><div class="wrap">
+    <div><a href="/">RENTV.com</a> · Commercial Real Estate News</div>
+    <div class="acts"><a href="/">Home</a><a href="/review">The REview</a><a class="sub" href="/">Subscribe</a></div>
+  </div></div>
+
+  <header class="mast"><div class="wrap">
+    <a href="/" class="logo">REN<span>TV</span>.com<small>Commercial Real Estate News</small></a>
+    <div class="datebox"><span>Western U.S. Edition</span><br><span id="today"></span></div>
+  </div></header>
+
+  <nav><div class="wrap">
+    <a href="/">Home</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><a href="/review">The REview</a>
+  </div></nav>
+
+  <div class="wrap"><article class="art" id="art">
+    <div class="crumb"><a href="/">Home</a> › <span>Story</span></div>
+    <span class="chip">RENTV · Western U.S.</span>
+    <h1 class="skl">Loading the story…<br>&nbsp;</h1>
+    <div class="byl">Fetching live from rentv.com…</div>
+    <div class="hero skl"></div>
+    <div class="body"><p class="skl">&nbsp;<br>&nbsp;<br>&nbsp;</p></div>
+  </article></div>
+
+  <footer><div class="wrap"><div class="cols">
+    <div><div class="logo">REN<span style="color:var(--red)">TV</span>.com</div><p style="max-width:34ch">Commercial real estate news, deals &amp; conferences across the Western United States since 1998.</p></div>
+    <div><h5>Sections</h5><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a></div>
+    <div><h5>More</h5><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a><a href="/review">The REview</a></div>
+    <div><h5>Company</h5><a href="/">Home</a><a href="/">Advertise</a><a href="/">Contact</a></div>
+  </div><div class="fine">© 2026 RENTV.com · Story served live from rentv.com</div></div></footer>
+
+<script>
+document.getElementById('today').textContent = new Date().toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});
+const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const id=(location.pathname.split('/').filter(Boolean).pop()||'').replace(/[^0-9]/g,'');
+const RENTV='https://www.rentv.com/content/homepage/mainnews/news/'+id;
+// Generic titles the reader proxy returns when rentv.com doesn't server-render the story.
+const GENERIC=/^RENTV\.com\b|commercial real estate news and property/i;
+async function load(){
+  const art=document.getElementById('art');
+  if(!id){art.innerHTML='<p class="err">Story not found. <a href="/">Back to the homepage →</a></p>';return;}
+  // Fetch the live feed (guaranteed correct headline/category) and the reader proxy (best-effort body) together.
+  const [feed, art0] = await Promise.allSettled([
+    fetch('/api/news').then(r=>r.json()),
+    fetch('/api/article/'+id).then(r=>r.ok?r.json():null)
+  ]);
+  const item = (feed.status==='fulfilled' ? (feed.value.items||[]).find(x=>String(x.id)===id) : null) || {};
+  const d = (art0.status==='fulfilled' && art0.value) ? art0.value : {};
+  // Prefer the feed's real headline; fall back to the proxy title only if it isn't the generic site title.
+  const title = item.title || (d.title && !GENERIC.test(d.title) ? d.title : 'RENTV Story');
+  const cat = item.cat || 'RENTV';
+  const src = item.url || d.url || RENTV;
+  const hasBody = Array.isArray(d.body) && d.body.length>0;
+  if(!item.title && !hasBody && art0.status!=='fulfilled'){
+    art.innerHTML='<p class="err">This story couldn’t be loaded right now. <a href="'+esc(RENTV)+'" target="_blank" rel="noopener">Read it on RENTV.com ↗</a><br><br><a href="/">← Back to the homepage</a></p>';
+    return;
+  }
+  document.title=title+' — RENTV.com';
+  const hero=(d.image && !GENERIC.test(d.image))?'<div class="hero"><img src="'+esc(d.image)+'" alt="" loading="lazy" onerror="this.parentNode.style.background=\'linear-gradient(135deg,#1f2937,#c8102e)\';this.remove()"></div>':'<div class="hero"></div>';
+  const body=hasBody
+    ? d.body.map(p=>'<p>'+esc(p)+'</p>').join('')
+    : '<p>This story is published in full on RENTV.com. Read the complete report — including deal terms, parties and broker details — at the source link below.</p>';
+  art.innerHTML=
+    '<div class="crumb"><a href="/">Home</a> › <a href="/'+esc(cat.toLowerCase())+'">'+esc(cat)+'</a> › <span>Story</span></div>'+
+    '<span class="chip">'+esc(cat)+' · Western U.S.</span>'+
+    '<h1>'+esc(title)+'</h1>'+
+    '<div class="byl">By RENTV Staff · live from rentv.com'+(d.fetched_at?(' · fetched '+new Date(d.fetched_at).toLocaleTimeString()):'')+'</div>'+
+    hero+
+    '<div class="body">'+body+'</div>'+
+    '<div class="src">📰 Republished live from RENTV.com. <a href="'+esc(src)+'" target="_blank" rel="noopener">View the full story on RENTV.com ↗</a></div>'+
+    '<a class="back" href="/">← Back to all CRE news</a>';
+}
+load();
+</script>
+</body>
+</html>
diff --git a/public/index.html b/public/index.html
index 11457e2a..4f7dde1f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -121,11 +121,11 @@ const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"
 function render(){
   const items = cat==='all'?ALL:ALL.filter(x=>x.cat===cat);
   const lead=items[0];
-  if(lead){document.getElementById('lead').innerHTML='<a href="'+esc(lead.url)+'" target="_blank" rel="noopener"><div class="img"></div><span class="chip">'+esc(lead.cat)+' · Western U.S.</span><h1>'+esc(lead.title)+'</h1><div class="byl">By RENTV Staff · live from rentv.com</div></a>';}
-  const cards=items.slice(1,13).map(x=>'<a class="card" href="'+esc(x.url)+'" target="_blank" rel="noopener"><div class="img"></div><span class="chip">'+esc(x.cat)+'</span><h4>'+esc(x.title)+'</h4><div class="byl">RENTV Staff</div></a>').join('');
+  if(lead){document.getElementById('lead').innerHTML='<a href="/news/'+encodeURIComponent(lead.id)+'"><div class="img"></div><span class="chip">'+esc(lead.cat)+' · Western U.S.</span><h1>'+esc(lead.title)+'</h1><div class="byl">By RENTV Staff · live from rentv.com</div></a>';}
+  const cards=items.slice(1,13).map(x=>'<a class="card" href="/news/'+encodeURIComponent(x.id)+'"><div class="img"></div><span class="chip">'+esc(x.cat)+'</span><h4>'+esc(x.title)+'</h4><div class="byl">RENTV Staff</div></a>').join('');
   document.getElementById('cards').innerHTML=cards||'<p style="color:var(--sub)">No items in this section right now.</p>';
   document.getElementById('breaking').innerHTML=ALL.slice(0,6).map(x=>'<span>'+esc(x.title)+'</span>').join(' · ').repeat(2);
-  document.getElementById('mostread').innerHTML=ALL.slice(0,5).map(x=>'<li><a href="'+esc(x.url)+'" target="_blank" rel="noopener">'+esc(x.title)+'</a></li>').join('');
+  document.getElementById('mostread').innerHTML=ALL.slice(0,5).map(x=>'<li><a href="/news/'+encodeURIComponent(x.id)+'">'+esc(x.title)+'</a></li>').join('');
 }
 async function load(){
   try{
diff --git a/public/section.html b/public/section.html
new file mode 100644
index 00000000..f2fbb714
--- /dev/null
+++ b/public/section.html
@@ -0,0 +1,116 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>RENTV.com — Section</title>
+<meta name="description" content="Commercial real estate news by section — RENTV.com.">
+<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,400;0,700;1,400&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
+<style>
+  :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--sans:'Inter',system-ui,sans-serif;--serif:'Playfair Display',Georgia,serif}
+  *{box-sizing:border-box;margin:0;padding:0}
+  body{font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.45}
+  a{color:inherit;text-decoration:none}
+  .wrap{max-width:1200px;margin:0 auto;padding:0 20px}
+  .util{background:var(--ink);color:#cfd4d9;font-size:12px}
+  .util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}
+  .util .acts a{margin-left:14px;color:#cfd4d9}.util .acts a.sub{color:#fff;background:var(--red);padding:4px 12px;border-radius:3px;font-weight:700}
+  .mast{border-bottom:2px solid var(--ink);padding:18px 0 12px}
+  .mast .wrap{display:flex;align-items:center;justify-content:space-between}
+  .logo{font-family:var(--serif);font-weight:700;font-size:34px;letter-spacing:-1px}.logo span{color:var(--red)}
+  .logo small{display:block;font-size:11px;font-weight:600;letter-spacing:3px;color:var(--sub);text-transform:uppercase;margin-top:2px;font-family:var(--sans)}
+  .datebox{font-size:12px;color:var(--sub);text-align:right}
+  nav{border-bottom:1px solid var(--line);position:sticky;top:0;background:#fff;z-index:10}
+  nav .wrap{display:flex;gap:2px;align-items:center;height:46px;overflow-x:auto}
+  nav a{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:14px 13px;border-bottom:3px solid transparent;white-space:nowrap;cursor:pointer}
+  nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
+  .head{padding:30px 0 6px}
+  .head .chip{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--red);margin-bottom:4px}
+  .head h1{font-family:var(--serif);font-size:34px;letter-spacing:-.5px}
+  .head .sub{font-size:13px;color:var(--sub);margin-top:6px}
+  .crumb{font-size:12px;color:var(--sub);padding:14px 0 0}.crumb a{color:var(--red);font-weight:700}
+  .cards{display:grid;grid-template-columns:1fr 1fr 1fr;gap:22px;padding:22px 0 10px}
+  .card{display:block}
+  .card .img{aspect-ratio:16/10;border-radius:5px;background:linear-gradient(135deg,#475569,#94a3b8)}
+  .card:nth-child(3n+2) .img{background:linear-gradient(135deg,#7c2d12,#c8102e)}
+  .card:nth-child(3n) .img{background:linear-gradient(135deg,#0f766e,#14b8a6)}
+  .card h4{font-family:var(--serif);font-size:18px;line-height:1.22;margin:10px 0 4px}
+  .card .byl{font-size:12px;color:var(--sub);margin-top:6px}
+  .live{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--sub);letter-spacing:.06em;text-transform:uppercase}
+  .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)}}
+  .empty{color:var(--sub);padding:40px 0}
+  .skl{background:linear-gradient(90deg,#eee,#f6f6f6,#eee);background-size:200% 100%;animation:sh 1.2s infinite;border-radius:4px;color:transparent}
+  @keyframes sh{to{background-position:-200% 0}}
+  footer{background:var(--ink);color:#aeb6bd;margin-top:36px;padding:40px 0 30px;font-size:13px}
+  footer .cols{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:28px}
+  footer .logo{color:#fff;font-size:26px;margin-bottom:8px}footer h5{color:#fff;font-size:12px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}footer a{display:block;padding:4px 0;color:#aeb6bd}
+  footer .fine{border-top:1px solid #2a2f34;margin-top:26px;padding-top:16px;font-size:12px;color:#7c848b}
+  @media(max-width:860px){.cards{grid-template-columns:1fr 1fr}}
+</style>
+</head>
+<body>
+  <div class="util"><div class="wrap">
+    <div><a href="/">RENTV.com</a> · Commercial Real Estate News</div>
+    <div class="acts"><a href="/">Home</a><a href="/review">The REview</a><a class="sub" href="/">Subscribe</a></div>
+  </div></div>
+
+  <header class="mast"><div class="wrap">
+    <a href="/" class="logo">REN<span>TV</span>.com<small>Commercial Real Estate News</small></a>
+    <div class="datebox"><span>Western U.S. Edition</span><br><span id="today"></span></div>
+  </div></header>
+
+  <nav><div class="wrap" id="nav">
+    <a href="/">Home</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><a href="/review">The REview</a>
+  </div></nav>
+
+  <div class="wrap">
+    <div class="crumb"><a href="/">Home</a> › <span id="crumbcat">Section</span></div>
+    <div class="head">
+      <span class="chip" id="chip">Section</span>
+      <h1 id="secname">Loading…</h1>
+      <div class="sub"><span class="live"><span class="dot"></span><span id="count">live from rentv.com</span></span></div>
+    </div>
+    <div class="cards" id="cards">
+      <div class="card"><div class="img skl"></div><h4 class="skl">&nbsp;</h4></div>
+      <div class="card"><div class="img skl"></div><h4 class="skl">&nbsp;</h4></div>
+      <div class="card"><div class="img skl"></div><h4 class="skl">&nbsp;</h4></div>
+    </div>
+  </div>
+
+  <footer><div class="wrap"><div class="cols">
+    <div><div class="logo">REN<span style="color:var(--red)">TV</span>.com</div><p style="max-width:34ch">Commercial real estate news, deals &amp; conferences across the Western United States since 1998.</p></div>
+    <div><h5>Sections</h5><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a></div>
+    <div><h5>More</h5><a href="/multifamily">Multifamily</a><a href="/industrial">Industrial</a><a href="/retail">Retail</a><a href="/review">The REview</a></div>
+    <div><h5>Company</h5><a href="/">Home</a><a href="/">Advertise</a><a href="/">Contact</a></div>
+  </div><div class="fine">© 2026 RENTV.com · Headlines refreshed live from rentv.com</div></div></footer>
+
+<script>
+document.getElementById('today').textContent = new Date().toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});
+const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+// Category is the last path segment: /financing -> Financing
+const slug=(location.pathname.split('/').filter(Boolean)[0]||'sales').toLowerCase();
+const CAT=slug.charAt(0).toUpperCase()+slug.slice(1);
+document.title='RENTV.com — '+CAT;
+document.getElementById('secname').textContent=CAT;
+document.getElementById('chip').textContent=CAT+' · Western U.S.';
+document.getElementById('crumbcat').textContent=CAT;
+// mark active nav
+document.querySelectorAll('#nav a').forEach(a=>{if(a.getAttribute('href')==='/'+slug)a.classList.add('on');});
+async function load(){
+  try{
+    const d=await (await fetch('/api/news')).json();
+    const items=(d.items||[]).filter(x=>(x.cat||'').toLowerCase()===slug);
+    const box=document.getElementById('cards');
+    if(!items.length){box.className='';box.innerHTML='<p class="empty">No '+esc(CAT)+' stories in the live feed right now. <a href="/" style="color:var(--red);font-weight:700">Back to the homepage →</a></p>';return;}
+    document.getElementById('count').textContent=items.length+' live '+(items.length===1?'story':'stories');
+    box.className='cards';
+    box.innerHTML=items.map(x=>'<a class="card" href="/news/'+encodeURIComponent(x.id)+'"><div class="img"></div><h4>'+esc(x.title)+'</h4><div class="byl">RENTV Staff · read in-shell</div></a>').join('');
+  }catch(e){
+    document.getElementById('cards').innerHTML='<p class="empty">Live feed temporarily unavailable — <a href="/" style="color:var(--red);font-weight:700">back to homepage</a>.</p>';
+  }
+}
+load();
+</script>
+</body>
+</html>

← daf98cbe rentv-website.agentabrams.com live: app-login-gated, latest  ·  back to Rentv  ·  Live Rates & Markets page (/markets.html): Treasury curve + e8f63f03 →