← back to Rentv Website
real rentv images (front page + article photos) + full article-body scraping + mobile hamburger nav
f8c4ddaeedbef0b970c53690f711e12cd2f9d839 · 2026-07-20 14:51:21 -0700 · Steve
Files touched
M data/news.jsonM data/ticker.jsonM public/article.htmlM public/index.htmlM public/section.htmlM scripts/pull-news.mjsM server.js
Diff
commit f8c4ddaeedbef0b970c53690f711e12cd2f9d839
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 20 14:51:21 2026 -0700
real rentv images (front page + article photos) + full article-body scraping + mobile hamburger nav
---
data/news.json | 92 ++++++++++++++++++++++++++++++++++-----------------
data/ticker.json | 2 +-
public/article.html | 19 +++++++++--
public/index.html | 28 ++++++++++++----
public/section.html | 23 ++++++++++---
scripts/pull-news.mjs | 6 +++-
server.js | 32 ++++++++++--------
7 files changed, 143 insertions(+), 59 deletions(-)
diff --git a/data/news.json b/data/news.json
index f5e1519..b9361f4 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,187 +1,217 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-07-20T21:06:12.438Z",
+ "fetched_at": "2026-07-20T21:46:22.972Z",
"count": 30,
"items": [
{
"id": "34218",
"title": "Camden Property Trust Buys Gilbert, AZ Res Community in $124 Mil Deal",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34218",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20278_3150ERayRdGilbertKimHoidalCBREJul132026.jpg"
},
{
"id": "34225",
"title": "CentrePoint Properties Spends $30 Mil on Trio of Colorado Retail Assets",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34225",
- "cat": "Retail"
+ "cat": "Retail",
+ "image": ""
},
{
"id": "34227",
"title": "MG Properties Buys 188-Unit Portland Res Asset",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34227",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20286_TupeloAlleyPortlandPaulKaseburgMGPropJul162026.jpg"
},
{
"id": "34216",
"title": "Meta Housing Well Underway with 300-Unit Res Community in Woodland Hills",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34216",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20277_21300WOxnardStWHillsHollyJenvyJul152026.jpg"
},
{
"id": "34214",
"title": "Intercontinental Real Estate Corporation Closes $69 Mil Retail Buy in Washington",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34214",
- "cat": "Retail"
+ "cat": "Retail",
+ "image": "https://www.rentv.com/images/dynamic/news_20274_1402LakeTappsPkwySEAuburnWABruceBeckJul152026.jpg"
},
{
"id": "34208",
"title": "Eagle Partners and The Vistria Group Spend $132 Mil on Garden Grove Mixed-Income Res Community",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34208",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20267_12091BayportStGardenGroveCOSTARAnnieNoebelCornersCommJul142026.jpg"
},
{
"id": "34204",
"title": "Oceanside Res Community Trades for $77.5 Mil",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34204",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20262_3851SherbourneDrOceansideDANKIRKSEYClaudiaLipmannCBREJul142026.jpg"
},
{
"id": "34210",
"title": "BKM Capital Partners Boosts Pac Northwest Holdings with Purchase of 400k sf Industrial Buy in Washington",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34210",
- "cat": "Industrial"
+ "cat": "Industrial",
+ "image": "https://www.rentv.com/images/dynamic/news_20270_SouthcenterWestBusinessParkTukwilaWASuleAygorenConnectJul142026.jpg"
},
{
"id": "34203",
"title": "The Bascom Group Pays Over $53 Mil for OC Res Community",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34203",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20261_6968CampusDrBuenaParkJerryFinkBascomJul132026.jpg"
},
{
"id": "34199",
"title": "Leasing Assignment for 650k sf San Francisco Office Tower Awarded to CBRE",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34199",
- "cat": "Sales"
+ "cat": "Sales",
+ "image": "https://www.rentv.com/images/dynamic/news_20259_415NatomaSFranciscoIrineSpivakCBREJun162026.jpg"
},
{
"id": "34202",
"title": "SummerHill Apartment Communities Breaks Ground on New 251-Unit Bay Area Res Development",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34202",
- "cat": "Development"
+ "cat": "Development",
+ "image": "https://www.rentv.com/images/dynamic/news_20260_11ElcaminoRealSMateoAnneMonaghanJul72026.jpg"
},
{
"id": "34196",
"title": "Rubicon Point Partners Spends $53 Mil on Silicon Valley Medical/Office Property",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34196",
- "cat": "Sales"
+ "cat": "Sales",
+ "image": ""
},
{
"id": "34195",
"title": "Borstein Enterprises Picks Up 282k sf OC Industrial Asset",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34195",
- "cat": "Industrial"
+ "cat": "Industrial",
+ "image": "https://www.rentv.com/images/dynamic/news_20256_15339BarrancaPkwyIrvineSavannahDurbinCWJul62026.jpg"
},
{
"id": "34188",
"title": "Chino Distribution Facility Leased by Auto Parts Distributor in $42 Mil Deal",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34188",
- "cat": "Leases"
+ "cat": "Leases",
+ "image": "https://www.rentv.com/images/dynamic/news_20252_16380EuclidAveChinoElisabethManvilleSmartJul22026.jpg"
},
{
"id": "34186",
"title": "Santa Clara Industrial Property Trades for $25.75 Mil",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34186",
- "cat": "Industrial"
+ "cat": "Industrial",
+ "image": "https://www.rentv.com/images/dynamic/news_20250_651MartinAveSClaraGraceLewisJLLJul12026.jpg"
},
{
"id": "34181",
"title": "Redwood West and Panattoni Buy The Pike Destination Center in Long Beach",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34181",
- "cat": "Retail"
+ "cat": "Retail",
+ "image": "https://www.rentv.com/images/dynamic/news_20245_95SPineAveLongBeachJessicaSpauldingJun302026.jpg"
},
{
"id": "34180",
"title": "Kennedy Wilson and Jamison Form Partnership to Develop 4,000 Units of Affordable Housing in Los Angeles",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34180",
- "cat": "Development"
+ "cat": "Development",
+ "image": "https://www.rentv.com/images/dynamic/news_20244_350SHopeBruceBeckJune302026.jpg"
},
{
"id": "34086",
"title": "Oak Park Investments and Arise Investments Acquire Westlake Village Warehouse/Office Building in $19 Mil Transaction",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34086",
- "cat": "Industrial"
+ "cat": "Industrial",
+ "image": "https://www.rentv.com/images/dynamic/news_20161_5785LinderoCanyonRdWstlkVillageDannySoroudiAriseJune22026.jpg"
},
{
"id": "34177",
"title": "Holland Partner Group Closes $105 Mil Bay Area Res Buy",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34177",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20242_1432WestSanCarlosSt.jpg"
},
{
"id": "34172",
"title": "Haven Housing buys 264-Unit Res Community in Austin Metro Area",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34172",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20237_15001StrathavenPassPflugervilleTXElliottGolanConnectJun262026.jpg"
},
{
"id": "34174",
"title": "MG Properties Acquires Two Multifamily Properties in Gresham, OR in Deals Totaling $60.8 Mil",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34174",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20239_ParksideApartmentsGreshamORPaulKasebergMGPropJune262026.jpg"
},
{
"id": "34171",
"title": "Leading AI Firm Leases 1.1 msf of Space at Luke Field in Glendale, AZ",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34171",
- "cat": "Leases"
+ "cat": "Leases",
+ "image": "https://www.rentv.com/images/dynamic/news_20236_7733LitchfieldRdGlendaleAZStaceyHershauerJun242026.jpg"
},
{
"id": "34169",
"title": "Seattle Area Res Community Refinanced with $82.5 Mil Loan from Mesa West Capital",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34169",
- "cat": "Financing"
+ "cat": "Financing",
+ "image": "https://www.rentv.com/images/dynamic/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg"
},
{
"id": "34160",
"title": "Apollo Lands in Gardena",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34160",
- "cat": "Sales"
+ "cat": "Sales",
+ "image": "https://www.rentv.com/images/dynamic/news_20227_12888CrenshawBlvdGardenaJadeTerryCityviewJun232026.jpg"
},
{
"id": "34165",
"title": "Phoenix Multifamily Community Lands New Owner",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34165",
- "cat": "Multifamily"
+ "cat": "Multifamily",
+ "image": "https://www.rentv.com/images/dynamic/news_20229_BroadstoneSeventhStPhnxKimHoidalCBREJun222026.jpg"
},
{
"id": "34158",
"title": "Fairfield Residential Wraps Up Work on Houston High-Rise Res Development",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34158",
- "cat": "Development"
+ "cat": "Development",
+ "image": "https://www.rentv.com/images/dynamic/news_20223_1010WaughDrHoustonAnneMonaghanJun232026.jpg"
},
{
"id": "34155",
"title": "Newly Constructed 1.18 msf AZ Industrial Campus Lands $122 Mil in Bridge Financing",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34155",
- "cat": "Financing"
+ "cat": "Financing",
+ "image": "https://www.rentv.com/images/dynamic/news_20219_TheBaseGlendaleAZCOSTARGraceLewisJLLJun222026.jpg"
},
{
"id": "34153",
"title": "BKM Capital Partners Closes $95 Mil SoCal Biz Campus Buy",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34153",
- "cat": "Sales"
+ "cat": "Sales",
+ "image": "https://www.rentv.com/images/dynamic/news_20218_CarsonIndustrialCntrCarsonSuleAygorenConnectJun222026.jpg"
},
{
"id": "34150",
"title": "Arizona Power Center Lands $38 Mil Refi",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34150",
- "cat": "Financing"
+ "cat": "Financing",
+ "image": "https://www.rentv.com/images/dynamic/news_20216_1639SStapleyRdMesaAZChrisEggerMarcomJun182026.jpg"
},
{
"id": "34148",
"title": "Southwest Traders Spends $40 Mil on Menifee Industrial Facility",
"url": "https://www.rentv.com/content/homepage/mainnews/news/34148",
- "cat": "Industrial"
+ "cat": "Industrial",
+ "image": "https://www.rentv.com/images/dynamic/news_20214_33520ZeidersRdMenifeeLeanneDalyColliersJun192026.jpg"
}
]
}
\ No newline at end of file
diff --git a/data/ticker.json b/data/ticker.json
index 275db1f..2fb0703 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-20T21:06:12.440Z",
+ "fetched_at": "2026-07-20T21:46:22.977Z",
"la_office_vac": {
"label": "LA OFFICE VAC",
"value": "23.1%",
diff --git a/public/article.html b/public/article.html
index 2a1d3dc..c42f4c9 100644
--- a/public/article.html
+++ b/public/article.html
@@ -21,9 +21,19 @@
.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 .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 4f7dde1..642e1be 100644
--- a/public/index.html
+++ b/public/index.html
@@ -22,9 +22,19 @@
.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 .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 +91,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 +131,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 +152,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 f2fbb71..2dacce4 100644
--- a/public/section.html
+++ b/public/section.html
@@ -21,9 +21,19 @@
.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 .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 5e853f3..4a15bf2 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 b9d494d..5ca272a 100644
--- a/server.js
+++ b/server.js
@@ -57,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) => {
← 2ed5be5 fork of rentv-v1 → independent rentv-website (port 9705); in
·
back to Rentv Website
·
center masthead wordmark (newspaper-style centered top) 0565bfb →