[object Object]

← back to Rentv 2026

TK-10569: port /api/advertise + /api/advertise-inquiries from rentv to rentv-2026

437aa93ce53586c3217e5df1562112931a02d7a6 · 2026-08-15 04:17:28 -0700 · steve@designerwallcoverings.com

Lead-capture endpoint was missing from feature/2026-overhaul-adslots branch.
Mirrors original rentv implementation; uses rentv-2026's existing writeThrottle
(10/hr per IP) instead of the manual counter pattern.

Files touched

Diff

commit 437aa93ce53586c3217e5df1562112931a02d7a6
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sat Aug 15 04:17:28 2026 -0700

    TK-10569: port /api/advertise + /api/advertise-inquiries from rentv to rentv-2026
    
    Lead-capture endpoint was missing from feature/2026-overhaul-adslots branch.
    Mirrors original rentv implementation; uses rentv-2026's existing writeThrottle
    (10/hr per IP) instead of the manual counter pattern.
---
 server.js | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/server.js b/server.js
index 89a406fc..66b231b3 100644
--- a/server.js
+++ b/server.js
@@ -835,6 +835,40 @@ app.get('/api/subscribers', adminOnly, (_q, res) => {
   res.json({ count: items.length, unique: uniq, items: items.slice(-500).reverse() });
 });
 
+// ── ADVERTISER INQUIRIES (/advertise "Request the media kit" form) ──────────
+// Inbound lead capture only — appends to an append-only JSONL, exactly like
+// /api/subscribe. Nothing is emailed to anyone; the read endpoint is admin-only.
+const ADV_INQ = process.env.RENTV_ADV_INQ_PATH || path.join(DATA, 'advertise-inquiries.jsonl');
+const MAX_ADV_INQ = 20000;
+let advInqCount = 0;
+try { advInqCount = fs.readFileSync(ADV_INQ, 'utf8').split('\n').filter(Boolean).length; } catch { /* none yet */ }
+app.post('/api/advertise', writeThrottle(10, 60 * 60 * 1000), (req, res) => {
+  const b = req.body || {};
+  const email = String(b.email || '').trim().toLowerCase();
+  const name = String(b.name || '').trim();
+  const company = String(b.company || '').trim();
+  if (!name || !company) return res.status(400).json({ ok: false, error: 'name and company required' });
+  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return res.status(400).json({ ok: false, error: 'valid work email required' });
+  if (advInqCount >= MAX_ADV_INQ) return res.status(503).json({ ok: false, error: 'inquiries temporarily closed' });
+  const xff = String(req.headers['x-forwarded-for'] || '').split(',').map(s => s.trim()).filter(Boolean);
+  const rec = {
+    name: name.slice(0, 120), company: company.slice(0, 160), email,
+    interest: String(b.interest || '').trim().slice(0, 80),
+    source: String(b.source || 'advertise').trim().slice(0, 40),
+    at: new Date().toISOString(),
+    ip: xff.length ? xff[xff.length - 1] : (req.socket.remoteAddress || ''),
+  };
+  try { fs.appendFileSync(ADV_INQ, JSON.stringify(rec) + '\n'); advInqCount++; }
+  catch { return res.status(500).json({ ok: false, error: 'could not save' }); }
+  res.json({ ok: true });
+});
+app.get('/api/advertise-inquiries', adminOnly, (_q, res) => {
+  let lines = [];
+  try { lines = fs.readFileSync(ADV_INQ, 'utf8').split('\n').filter(Boolean); } catch { /* none yet */ }
+  const items = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
+  res.json({ count: items.length, items: items.slice(-500).reverse() });
+});
+
 // ── BLOG BACKEND — original RENTV articles (file-backed, no external DB). ──
 const POSTS = path.join(DATA, 'posts.json');
 const readPosts = () => { try { return JSON.parse(fs.readFileSync(POSTS, 'utf8')); } catch { return []; } };

← 5a59077e rentv: per-IP write-form rate limits on /api/subscribe + /ap  ·  back to Rentv 2026  ·  initial scaffold + creds-safe fetch guard (gitify per standi f19f6ee4 →