← back to Rentv 2026
parity with rentv-website: real rentv images + full article-body scraping + mobile hamburger nav + centered masthead
bda422481697cce01f45e2f331b2596625de395e · 2026-07-20 14:54:56 -0700 · Steve Abrams
Files touched
M data/markets.jsonM data/news.jsonM data/ticker.jsonM public/article.htmlM public/index.htmlM public/section.htmlM scripts/pull-news.mjsM server.js
Diff
commit bda422481697cce01f45e2f331b2596625de395e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 20 14:54:56 2026 -0700
parity with rentv-website: real rentv images + full article-body scraping + mobile hamburger nav + centered masthead
---
data/markets.json | 2 +-
data/news.json | 2 +-
data/ticker.json | 2 +-
public/article.html | 23 +++++++++++++++++++----
public/index.html | 33 +++++++++++++++++++++++++--------
public/section.html | 27 +++++++++++++++++++++------
scripts/pull-news.mjs | 6 +++++-
server.js | 38 ++++++++++++++++++--------------------
8 files changed, 91 insertions(+), 42 deletions(-)
diff --git a/data/markets.json b/data/markets.json
index 58fc6db9..cdfd6a9d 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-20T21:27:36.267Z",
+ "fetched_at": "2026-07-20T21:42:23.756Z",
"source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
"curve": [
{
diff --git a/data/news.json b/data/news.json
index 70b6092c..9fcea8d0 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-07-20T21:22:22.127Z",
+ "fetched_at": "2026-07-20T21:42:23.433Z",
"count": 30,
"items": [
{
diff --git a/data/ticker.json b/data/ticker.json
index a7f7274b..88696278 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-20T21:27:36.244Z",
+ "fetched_at": "2026-07-20T21:42:23.730Z",
"la_office_vac": {
"label": "LA OFFICE VAC",
"value": "23.1%",
diff --git a/public/article.html b/public/article.html
index 2a1d3dcf..5b8882b4 100644
--- a/public/article.html
+++ b/public/article.html
@@ -16,14 +16,24 @@
.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}
+ .mast .wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:3px}
.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}
+ .datebox{font-size:12px;color:var(--sub);text-align:center}
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 .wrap{display:flex;gap:2px;align-items:center;height:46px}
+ .ham{display:none;background:none;border:0;font-size:24px;line-height:1;color:var(--ink);cursor:pointer;padding:8px 12px 8px 0;margin-right:4px}
+ .navlinks{display:flex;gap:2px;align-items:center;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)}
+ @media(max-width:860px){
+ .ham{display:block;order:-1}
+ nav .wrap{height:52px}
+ .navlinks{display:none;position:absolute;top:52px;left:0;right:0;background:#fff;flex-direction:column;align-items:stretch;border-bottom:2px solid var(--ink);box-shadow:0 10px 24px rgba(0,0,0,.14);z-index:30;max-height:78vh;overflow-y:auto}
+ .navlinks.open{display:flex}
+ .navlinks a{padding:15px 20px;border-bottom:1px solid var(--line);border-left:4px solid transparent}
+ .navlinks a:hover{color:var(--red);border-left-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}
@@ -58,7 +68,10 @@
</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>
+ <button class="ham" id="hamBtn" aria-label="Menu" aria-expanded="false">☰</button>
+ <div class="navlinks" 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>
</div></nav>
<div class="wrap"><article class="art" id="art">
@@ -118,6 +131,8 @@ async function load(){
'<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>';
}
+const hamBtn=document.getElementById('hamBtn');
+hamBtn.addEventListener('click',()=>{const open=document.getElementById('nav').classList.toggle('open');hamBtn.setAttribute('aria-expanded',open);});
load();
</script>
</body>
diff --git a/public/index.html b/public/index.html
index 4f7dde1f..b405db1c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -17,14 +17,25 @@
.ticker{display:flex;gap:20px;overflow:hidden;white-space:nowrap}.ticker b{color:#fff}.up{color:#4ade80}.dn{color:#f87171}
.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}
+ .mast{padding:20px 0 14px}
+ .mast .wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:3px}
.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}
+ .datebox{font-size:12px;color:var(--sub);text-align:center}
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 .wrap{display:flex;gap:2px;align-items:center;height:46px}
+ .ham{display:none;background:none;border:0;font-size:24px;line-height:1;color:var(--ink);cursor:pointer;padding:8px 12px 8px 0;margin-right:4px}
+ .navlinks{display:flex;gap:2px;align-items:center;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)}
+ @media(max-width:860px){
+ .ham{display:block;order:-1}
+ nav .wrap{height:52px}
+ .navlinks{display:none;position:absolute;top:52px;left:0;right:0;background:#fff;flex-direction:column;align-items:stretch;border-bottom:2px solid var(--ink);box-shadow:0 10px 24px rgba(0,0,0,.14);z-index:30;max-height:78vh;overflow-y:auto}
+ .navlinks.open{display:flex}
+ .navlinks a{padding:15px 20px;border-bottom:1px solid var(--line);border-left:4px solid transparent}
+ .navlinks a:hover,.navlinks a.on{color:var(--red);border-bottom:1px solid var(--line);border-left-color:var(--red)}
+ }
.breaking{background:var(--red);color:#fff;font-size:13px}
.breaking .wrap{display:flex;align-items:center;gap:14px;height:40px;overflow:hidden}
.breaking .tag{background:#fff;color:var(--red);font-weight:800;text-transform:uppercase;font-size:11px;letter-spacing:.08em;padding:3px 9px;border-radius:2px;flex:none}
@@ -81,8 +92,11 @@
<div class="datebox"><span id="edition">Western U.S. Edition</span><br><span id="today"></span></div>
</div></header>
- <nav><div class="wrap" id="nav">
- <a data-cat="all" class="on">Home</a><a data-cat="Sales">Sales</a><a data-cat="Leases">Leases</a><a data-cat="Financing">Financing</a><a data-cat="Development">Development</a><a data-cat="Multifamily">Multifamily</a><a data-cat="Industrial">Industrial</a><a data-cat="Retail">Retail</a><a href="/review">The REview</a><a href="#conf">Conferences</a>
+ <nav><div class="wrap">
+ <button class="ham" id="hamBtn" aria-label="Menu" aria-expanded="false">☰</button>
+ <div class="navlinks" id="nav">
+ <a data-cat="all" class="on">Home</a><a data-cat="Sales">Sales</a><a data-cat="Leases">Leases</a><a data-cat="Financing">Financing</a><a data-cat="Development">Development</a><a data-cat="Multifamily">Multifamily</a><a data-cat="Industrial">Industrial</a><a data-cat="Retail">Retail</a><a href="/review">The REview</a><a href="#conf">Conferences</a>
+ </div>
</div></nav>
<div class="breaking"><div class="wrap">
@@ -118,11 +132,12 @@
document.getElementById('today').textContent = new Date().toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});
let ALL=[], cat='all';
const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+const bgimg=o=>(o&&o.image)?(' style="background:#12161a url(\''+esc(o.image)+'\') center/cover"'):'';
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="/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('');
+ if(lead){document.getElementById('lead').innerHTML='<a href="/news/'+encodeURIComponent(lead.id)+'"><div class="img"'+bgimg(lead)+'></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"'+bgimg(x)+'></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="/news/'+encodeURIComponent(x.id)+'">'+esc(x.title)+'</a></li>').join('');
@@ -138,7 +153,9 @@ async function load(){
}
}catch(e){document.getElementById('lead').querySelector('.byl').textContent='Live feed temporarily unavailable — retrying…';}
}
-document.getElementById('nav').addEventListener('click',e=>{const a=e.target.closest('a[data-cat]');if(!a)return;e.preventDefault();document.querySelectorAll('#nav a').forEach(x=>x.classList.remove('on'));a.classList.add('on');cat=a.dataset.cat;render();window.scrollTo({top:0,behavior:'smooth'});});
+const hamBtn=document.getElementById('hamBtn');
+hamBtn.addEventListener('click',()=>{const open=document.getElementById('nav').classList.toggle('open');hamBtn.setAttribute('aria-expanded',open);});
+document.getElementById('nav').addEventListener('click',e=>{const a=e.target.closest('a');if(!a)return;document.getElementById('nav').classList.remove('open');hamBtn.setAttribute('aria-expanded','false');if(!a.dataset.cat)return;e.preventDefault();document.querySelectorAll('#nav a').forEach(x=>x.classList.remove('on'));a.classList.add('on');cat=a.dataset.cat;render();window.scrollTo({top:0,behavior:'smooth'});});
document.getElementById('subbtn').onclick=()=>{if((document.getElementById('email').value||'').includes('@')){document.getElementById('subok').style.display='block';}};
async function loadTicker(){
try{
diff --git a/public/section.html b/public/section.html
index f2fbb714..f901801b 100644
--- a/public/section.html
+++ b/public/section.html
@@ -16,14 +16,24 @@
.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}
+ .mast .wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:3px}
.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}
+ .datebox{font-size:12px;color:var(--sub);text-align:center}
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 .wrap{display:flex;gap:2px;align-items:center;height:46px}
+ .ham{display:none;background:none;border:0;font-size:24px;line-height:1;color:var(--ink);cursor:pointer;padding:8px 12px 8px 0;margin-right:4px}
+ .navlinks{display:flex;gap:2px;align-items:center;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)}
+ @media(max-width:860px){
+ .ham{display:block;order:-1}
+ nav .wrap{height:52px}
+ .navlinks{display:none;position:absolute;top:52px;left:0;right:0;background:#fff;flex-direction:column;align-items:stretch;border-bottom:2px solid var(--ink);box-shadow:0 10px 24px rgba(0,0,0,.14);z-index:30;max-height:78vh;overflow-y:auto}
+ .navlinks.open{display:flex}
+ .navlinks a{padding:15px 20px;border-bottom:1px solid var(--line);border-left:4px solid transparent}
+ .navlinks a:hover,.navlinks a.on{color:var(--red);border-bottom:1px solid var(--line);border-left-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}
@@ -60,8 +70,11 @@
<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>
+ <nav><div class="wrap">
+ <button class="ham" id="hamBtn" aria-label="Menu" aria-expanded="false">☰</button>
+ <div class="navlinks" 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>
</div></nav>
<div class="wrap">
@@ -97,6 +110,8 @@ 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');});
+const hamBtn=document.getElementById('hamBtn');
+hamBtn.addEventListener('click',()=>{const open=document.getElementById('nav').classList.toggle('open');hamBtn.setAttribute('aria-expanded',open);});
async function load(){
try{
const d=await (await fetch('/api/news')).json();
@@ -105,7 +120,7 @@ async function load(){
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('');
+ box.innerHTML=items.map(x=>'<a class="card" href="/news/'+encodeURIComponent(x.id)+'"><div class="img"'+((x.image)?(' style="background:#12161a url(\''+esc(x.image)+'\') center/cover"'):'')+'></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>';
}
diff --git a/scripts/pull-news.mjs b/scripts/pull-news.mjs
index 5e853f30..4a15bf26 100644
--- a/scripts/pull-news.mjs
+++ b/scripts/pull-news.mjs
@@ -18,6 +18,10 @@ const clean = (t) => t.replace(/&/g,'&').replace(/'/g,"'").replace(/&quo
try {
const html = await fetchDecoded('https://www.rentv.com/');
+ // Map each story id -> its real property photo (linked-image form: <a href='...news/ID'><img src='.../images/dynamic/news_*.jpg'>)
+ const imgMap = {};
+ const ire = /href=['"][^'"]*\/news\/(\d+)['"]\s*>\s*<img[^>]+src=['"]([^'"]+\/images\/dynamic\/news_[^'"]+\.(?:jpe?g|png))['"]/gi;
+ let im; while ((im = ire.exec(html))) { if (!imgMap[im[1]]) imgMap[im[1]] = im[2].startsWith('http') ? im[2] : 'https://www.rentv.com/' + im[2].replace(/^\//, ''); }
const re = /<a[^>]+href="([^"]*content\/homepage\/mainnews\/news\/(\d+))"[^>]*>([^<]{20,160})<\/a>/gi;
const seen = new Set(), items = []; let m;
while ((m = re.exec(html)) && items.length < 30) {
@@ -32,7 +36,7 @@ try {
: /res |multifamily|apartment|unit/.test(t) ? 'Multifamily'
: /industrial|warehouse|logistics/.test(t) ? 'Industrial'
: 'Sales';
- items.push({ id, title, url: m[1].startsWith('http') ? m[1] : 'https://www.rentv.com/' + m[1].replace(/^\//,''), cat });
+ items.push({ id, title, url: m[1].startsWith('http') ? m[1] : 'https://www.rentv.com/' + m[1].replace(/^\//,''), cat, image: imgMap[id] || '' });
}
const payload = { source: 'rentv.com — live', fetched_at: new Date().toISOString(), count: items.length, items };
writeFileSync(join(OUT, 'news.json'), JSON.stringify(payload, null, 1));
diff --git a/server.js b/server.js
index 897d92d7..4e2ac351 100644
--- a/server.js
+++ b/server.js
@@ -34,12 +34,6 @@ app.get('/api/ticker', (_q, r) => {
r.json(readJSON('ticker.json', {}));
});
-// ── Rates & Markets dashboard data (Treasury curve + REIT indices, live via Yahoo) ──
-app.get('/api/markets', (_q, r) => {
- r.set('Cache-Control', 'public, max-age=120');
- r.json(readJSON('markets.json', {}));
-});
-
// ── The REview: live CRE video catalog (synced from rentvreview.com by cron) ──
app.get('/api/videos', (_q, r) => {
r.set('Cache-Control', 'public, max-age=300');
@@ -63,22 +57,26 @@ async function fetchArticle(id) {
let html = buf.toString('utf8');
if ((html.match(/�/g) || []).length > 5) html = buf.toString('latin1');
- const og = (p) => { const m = html.match(new RegExp('<meta[^>]+property=["\']og:' + p + '["\'][^>]+content=["\']([^"\']+)', 'i')); return m ? decode(m[1]) : ''; };
- let title = og('title') || (html.match(/<title>([^<]+)<\/title>/i)?.[1] || '').replace(/\s*[|\-–]\s*RENTV.*/i, '');
- title = decode(title);
- const image = og('image');
+ // RENTV is a classic server-rendered table site — the story lives in <td class="header"> + following cells,
+ // and the real property photo is a /images/dynamic/news_*.jpg. Extract those directly (no <p> tags exist).
+ const hM = html.match(/<td class="header"[^>]*>([\s\S]*?)<\/td>/i);
+ let title = hM ? stripTags(hM[1]) : (html.match(/<title>([^<]+)<\/title>/i)?.[1] || 'RENTV Story');
+ title = decode(title).replace(/\s*[|\-–]\s*RENTV.*/i, '') || 'RENTV Story';
- // Body: grab the largest cluster of <p> blocks (the article copy).
- const paras = [];
- const pre = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
- let m; const pRe = /<p[^>]*>([\s\S]*?)<\/p>/gi;
- while ((m = pRe.exec(pre))) {
- const txt = stripTags(m[1]);
- if (txt.length >= 60 && !/^(©|copyright|all rights|advertise|subscribe|follow us)/i.test(txt)) paras.push(txt);
- }
- // de-dupe + cap
+ const imM = html.match(/["'](\/?(?:https?:\/\/[^"']*)?\/?images\/dynamic\/news_\d+_[^"']+\.(?:jpe?g|png))["']/i);
+ let image = imM ? imM[1] : '';
+ if (image && !/^https?:/i.test(image)) image = 'https://www.rentv.com/' + image.replace(/^\//, '');
+
+ // Body: take the region from the headline to a trailing marker, split on <br><br> into real paragraphs.
+ const start = hM ? hM.index + hM[0].length : 0;
+ let region = html.slice(start);
+ const endM = region.search(/Return to|class="header-column"|Comment on this|Source:\s|id="footer"|<!--\s*footer/i);
+ if (endM > 0) region = region.slice(0, endM);
+ const paras = region.split(/<br\s*\/?>\s*(?: \s*)?<br\s*\/?>/i)
+ .map(chunk => stripTags(chunk).replace(/^\d{1,2}\/\d{1,2}\/\d{2,4}\s*/, '').trim())
+ .filter(t => t.length >= 40 && !/^(©|copyright|all rights|advertise|subscribe|follow us|return to)/i.test(t));
const body = [...new Set(paras)].slice(0, 40);
- return { id, url, title: title || 'RENTV Story', image, body, fetched_at: new Date().toISOString() };
+ return { id, url, title, image, body, fetched_at: new Date().toISOString() };
}
app.get('/api/article/:id', async (req, res) => {
← 71395b46 auto-save: 2026-07-20T14:32:39 (2 files) — data/news.json da
·
back to Rentv 2026
·
Markets: durable additive components (markets.html + pull-ma e431d2ff →