← back to Rentv 2026
auto-save: 2026-07-19T14:56:49 (2 files) — data/news.json server.js
3db1c038c03174f9fb3ed0600e50042d1253ba2a · 2026-07-19 14:56:52 -0700 · Steve Abrams
Files touched
M data/news.jsonM server.js
Diff
commit 3db1c038c03174f9fb3ed0600e50042d1253ba2a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 19 14:56:52 2026 -0700
auto-save: 2026-07-19T14:56:49 (2 files) — data/news.json server.js
---
data/news.json | 2 +-
server.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 77 insertions(+), 9 deletions(-)
diff --git a/data/news.json b/data/news.json
index 5ffef7a4..a28ca05f 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-07-19T21:21:35.861Z",
+ "fetched_at": "2026-07-19T21:41:36.401Z",
"count": 30,
"items": [
{
diff --git a/server.js b/server.js
index 0afa1df0..1241bd50 100644
--- a/server.js
+++ b/server.js
@@ -1,20 +1,88 @@
'use strict';
-// rentv-v1 — gated live-data CRE-news site (The Wire concept) on LIVE rentv.com data.
-const fs = require('fs'); const path = require('path'); const express = require('express');
-const app = express(); const PORT = process.env.PORT || 9704;
+// rentv-v1 — full CRE-news website rebuild on LIVE rentv.com data.
+// One live-data engine (news.json, refreshed by cron) + an on-demand article
+// reader proxy (/api/article/:id) so stories are read INSIDE our modern shell.
+const fs = require('fs');
+const path = require('path');
+const express = require('express');
+const app = express();
+const PORT = process.env.PORT || 9704;
const DATA = path.join(__dirname, 'data');
+const PUB = path.join(__dirname, 'public');
const readJSON = (f, fb) => { try { return JSON.parse(fs.readFileSync(path.join(DATA, f), 'utf8')); } catch { return fb; } };
-// Basic Auth gate (unified + client logins). Health stays open for deploy smoke-tests.
+// ── Basic Auth gate (unified + client logins). Health stays open for smoke-tests. ──
const CREDS = ['admin:DW2024!', 'Boomer:Rentv2024!']
.concat((process.env.BASIC_AUTH_EXTRA || '').split(',').map(s => s.trim()).filter(Boolean));
const ACCEPTED = new Set(CREDS.map(c => 'Basic ' + Buffer.from(c).toString('base64')));
app.get('/api/health', (_q, r) => r.json({ ok: true, at: new Date().toISOString() }));
app.use((req, res, next) => {
if (ACCEPTED.has(req.headers.authorization || '')) return next();
- res.set('WWW-Authenticate', 'Basic realm="RENTV v1"');
+ res.set('WWW-Authenticate', 'Basic realm="RENTV"');
return res.status(401).send('Authentication required');
});
-app.get('/api/news', (_q, r) => { r.set('Cache-Control', 'public, max-age=120'); r.json(readJSON('news.json', { items: [] })); });
-app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] }));
-app.listen(PORT, () => console.log('rentv-v1 (gated, live data) on ' + PORT));
+
+// ── Live news feed (served from the cron-refreshed cache) ──
+app.get('/api/news', (_q, r) => {
+ r.set('Cache-Control', 'public, max-age=120');
+ r.json(readJSON('news.json', { items: [] }));
+});
+
+// ── Article reader proxy: fetch a single rentv.com story on demand + cache it. ──
+const artCache = new Map(); // id -> { at, data }
+const ART_TTL = 15 * 60 * 1000;
+const decode = (t) => (t || '')
+ .replace(/&/g, '&').replace(/�?39;/g, "'").replace(/’/g, "'").replace(/‘/g, "'")
+ .replace(/"/g, '"').replace(/“/g, '"').replace(/”/g, '"')
+ .replace(/ /g, ' ').replace(/�?45;/g, '-').replace(/—/g, '—').replace(/–/g, '–')
+ .replace(/&/g, '&').replace(/\s+/g, ' ').trim();
+const stripTags = (h) => decode(h.replace(/<[^>]+>/g, ' '));
+
+async function fetchArticle(id) {
+ const url = 'https://www.rentv.com/content/homepage/mainnews/news/' + encodeURIComponent(id);
+ const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 RENTV-reader' }, signal: AbortSignal.timeout(15000) });
+ const buf = Buffer.from(await r.arrayBuffer());
+ 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');
+
+ // 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 body = [...new Set(paras)].slice(0, 40);
+ return { id, url, title: title || 'RENTV Story', image, body, fetched_at: new Date().toISOString() };
+}
+
+app.get('/api/article/:id', async (req, res) => {
+ const id = String(req.params.id).replace(/[^0-9]/g, '');
+ if (!id) return res.status(400).json({ error: 'bad id' });
+ const hit = artCache.get(id);
+ if (hit && Date.now() - hit.at < ART_TTL) { res.set('Cache-Control', 'public, max-age=300'); return res.json(hit.data); }
+ try {
+ const data = await fetchArticle(id);
+ artCache.set(id, { at: Date.now(), data });
+ res.set('Cache-Control', 'public, max-age=300');
+ res.json(data);
+ } catch (e) {
+ if (hit) return res.json(hit.data); // serve stale rather than fail
+ res.status(502).json({ error: 'fetch failed', id, url: 'https://www.rentv.com/content/homepage/mainnews/news/' + id });
+ }
+});
+
+// ── Clean section routes → the one section template (category read client-side from path) ──
+const SECTIONS = ['financing', 'leases', 'development', 'retail', 'multifamily', 'industrial', 'sales'];
+SECTIONS.forEach(s => app.get('/' + s, (_q, r) => r.sendFile(path.join(PUB, 'section.html'))));
+app.get('/news/:id', (_q, r) => r.sendFile(path.join(PUB, 'article.html')));
+
+app.use(express.static(PUB, { extensions: ['html'] }));
+app.listen(PORT, () => console.log('rentv-v1 (full site, gated, live data) on ' + PORT));
← 846bfda3 auto-save: 2026-07-19T14:26:43 (1 files) — data/news.json
·
back to Rentv 2026
·
auto-save: 2026-07-19T15:26:55 (1 files) — data/news.json a0c55d81 →