[object Object]

← back to Rentv

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

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(/&amp;/g, '&').replace(/&#0?39;/g, "'").replace(/&rsquo;/g, "'").replace(/&lsquo;/g, "'")
+  .replace(/&quot;/g, '"').replace(/&ldquo;/g, '"').replace(/&rdquo;/g, '"')
+  .replace(/&nbsp;/g, ' ').replace(/&#0?45;/g, '-').replace(/&mdash;/g, '—').replace(/&ndash;/g, '–')
+  .replace(/&amp;/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  ·  auto-save: 2026-07-19T15:26:55 (1 files) — data/news.json a0c55d81 →