← back to Rentv 2026
rentv-v1: harden newsletter capture per contrarian — idempotent dedup (hot email Set), MAX_SUBS 50k ceiling, rightmost-XFF ip; honest CRE Talk grid framing (broader REview catalog, not implied interviews-only)
255f3538d9525d9e0064b94fb63dcc37e1f373e2 · 2026-07-21 13:18:14 -0700 · Steve Abrams
Files touched
M build.mjsM public/cre-talk.htmlM server.js
Diff
commit 255f3538d9525d9e0064b94fb63dcc37e1f373e2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 21 13:18:14 2026 -0700
rentv-v1: harden newsletter capture per contrarian — idempotent dedup (hot email Set), MAX_SUBS 50k ceiling, rightmost-XFF ip; honest CRE Talk grid framing (broader REview catalog, not implied interviews-only)
---
build.mjs | 2 +-
public/cre-talk.html | 2 +-
server.js | 17 +++++++++++++++--
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/build.mjs b/build.mjs
index 6cbb89d5..e3cee585 100644
--- a/build.mjs
+++ b/build.mjs
@@ -298,7 +298,7 @@ const creTalk = page('CRE Talk', 'CRE Talk — RENTV\'s interview series with th
</div></section>
<section class="blk"><div class="wrap">
<div class="eyebrow rv">Watch</div><h2 class="big rv">From The REview.</h2>
- <p class="lead2 rv">The latest from RENTV's live video catalog. New CRE Talk conversations post here as they're recorded.</p>
+ <p class="lead2 rv">CRE Talk episodes live on The REview, alongside RENTV's full video catalog — market panels, property features and deal breakdowns. Here's what's fresh right now.</p>
<div class="cols3" id="vidgrid" style="margin-top:26px"><p style="color:var(--sub)">Loading the latest videos…</p></div>
<a class="cta rv" href="/review" style="margin-top:26px">See the full REview catalog →</a>
</div></section>
diff --git a/public/cre-talk.html b/public/cre-talk.html
index 8c933472..00fbfe18 100644
--- a/public/cre-talk.html
+++ b/public/cre-talk.html
@@ -100,7 +100,7 @@
</div></section>
<section class="blk"><div class="wrap">
<div class="eyebrow rv">Watch</div><h2 class="big rv">From The REview.</h2>
- <p class="lead2 rv">The latest from RENTV's live video catalog. New CRE Talk conversations post here as they're recorded.</p>
+ <p class="lead2 rv">CRE Talk episodes live on The REview, alongside RENTV's full video catalog — market panels, property features and deal breakdowns. Here's what's fresh right now.</p>
<div class="cols3" id="vidgrid" style="margin-top:26px"><p style="color:var(--sub)">Loading the latest videos…</p></div>
<a class="cta rv" href="/review" style="margin-top:26px">See the full REview catalog →</a>
</div></section>
diff --git a/server.js b/server.js
index c95c3814..b6ff72db 100644
--- a/server.js
+++ b/server.js
@@ -50,19 +50,32 @@ app.get('/api/markets', (_q, r) => {
// ── Newsletter capture: append-only local JSONL (NO external send-to-list). ──
const SUBS = path.join(DATA, 'subscribers.jsonl');
+const MAX_SUBS = 50000; // hard ceiling — guards against disk-fill / append abuse
+// Hot set of already-captured emails, loaded once at startup, for idempotent capture.
+const subSet = new Set();
+try {
+ fs.readFileSync(SUBS, 'utf8').split('\n').filter(Boolean).forEach(l => {
+ try { const e = JSON.parse(l).email; if (e) subSet.add(e); } catch { /* skip */ }
+ });
+} catch { /* no file yet */ }
app.post('/api/subscribe', (req, res) => {
const b = req.body || {};
const email = String(b.email || '').trim().toLowerCase();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return res.status(400).json({ ok: false, error: 'valid email required' });
+ // Idempotent: already on the list → succeed silently, no duplicate row.
+ if (subSet.has(email)) return res.json({ ok: true, already: true });
+ if (subSet.size >= MAX_SUBS) return res.status(503).json({ ok: false, error: 'list temporarily closed' });
+ // x-forwarded-for: behind nginx the real client IP is the LAST segment (client-supplied values precede it).
+ const xff = String(req.headers['x-forwarded-for'] || '').split(',').map(s => s.trim()).filter(Boolean);
const rec = {
email,
name: String(b.name || '').trim().slice(0, 120),
interest: String(b.interest || '').trim().slice(0, 60),
source: String(b.source || 'site').trim().slice(0, 40),
at: new Date().toISOString(),
- ip: (req.headers['x-forwarded-for'] || req.socket.remoteAddress || '').toString().split(',')[0].trim(),
+ ip: xff.length ? xff[xff.length - 1] : (req.socket.remoteAddress || ''), // best-effort, untrusted
};
- try { fs.appendFileSync(SUBS, JSON.stringify(rec) + '\n'); }
+ try { fs.appendFileSync(SUBS, JSON.stringify(rec) + '\n'); subSet.add(email); }
catch (e) { return res.status(500).json({ ok: false, error: 'could not save' }); }
res.json({ ok: true });
});
← 59bb51bc rentv-v1: add /cre-talk interview-series page (live REview v
·
back to Rentv 2026
·
rentv-v1 deploy: exclude data/subscribers.jsonl from rsync s e58b9691 →