[object Object]

← back to Rentv

syndicate: RSS 2.0 + JSON Feed for news (/feed.xml, /feed.json) + deals RSS (/deals.xml), internal links only; /syndicate page documents feeds with copy buttons + live preview; wired into Hub + nav

3c14210a59ec6091cfe80da7207a82e4a05565b6 · 2026-07-24 07:45:00 -0700 · Steve

Files touched

Diff

commit 3c14210a59ec6091cfe80da7207a82e4a05565b6
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 24 07:45:00 2026 -0700

    syndicate: RSS 2.0 + JSON Feed for news (/feed.xml, /feed.json) + deals RSS (/deals.xml), internal links only; /syndicate page documents feeds with copy buttons + live preview; wired into Hub + nav
---
 public/hub.html            |   1 +
 public/index.html          |   2 +-
 public/syndicate.html      | 150 +++++++++++++++++++++++++++++++++++++++++++++
 server.js                  |  46 ++++++++++++++
 tools/specs/syndicate.json |  23 +++++++
 5 files changed, 221 insertions(+), 1 deletion(-)

diff --git a/public/hub.html b/public/hub.html
index 3595c6ab..303288b0 100644
--- a/public/hub.html
+++ b/public/hub.html
@@ -119,6 +119,7 @@ const TOOLS={
   ],
   read:[
     {ic:'📰',t:'The RENTV Brief',d:'A shareable, print-ready snapshot of the market — perfect to forward.',h:'/brief'},
+    {ic:'📡',t:'Syndicate',d:'RSS & JSON feeds to pull RENTV into your reader, site or newsletter.',h:'/syndicate'},
   ],
 };
 const card=t=>`<a class="card" href="${t.h}"><div class="ic">${t.ic}</div><h3>${t.t}</h3><p>${t.d}</p><div class="go">Open</div></a>`;
diff --git a/public/index.html b/public/index.html
index 3d9efdb4..b3800ca1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -104,7 +104,7 @@
   <div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose" aria-label="Close">✕</button></div>
   <div class="snap" id="snap"></div>
   <div class="grp"><div class="gt">Discover</div></div>
-  <nav><a href="/hub" style="font-weight:700">✨ Intelligence Hub — all tools</a><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/sector">🏙️ Sector Deep-Dives</a><a href="/metros">📍 Deal Flow by Metro</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a><a href="/directory">🏢 CRE Directory</a><a href="/compare">⚖️ Deal Comparison</a><a href="/brief">📰 The RENTV Brief</a></nav>
+  <nav><a href="/hub" style="font-weight:700">✨ Intelligence Hub — all tools</a><a href="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</a><a href="/sector">🏙️ Sector Deep-Dives</a><a href="/metros">📍 Deal Flow by Metro</a><a href="/tools">🧮 CRE Toolkit</a><a href="/watchlist">⭐ Deal Watchlist</a><a href="/directory">🏢 CRE Directory</a><a href="/compare">⚖️ Deal Comparison</a><a href="/brief">📰 The RENTV Brief</a><a href="/syndicate">📡 Syndicate</a></nav>
   <div class="grp"><div class="gt">Sections</div></div>
   <nav><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></nav>
   <div class="grp"><div class="gt">Intelligence</div></div>
diff --git a/public/syndicate.html b/public/syndicate.html
new file mode 100644
index 00000000..a1ee9664
--- /dev/null
+++ b/public/syndicate.html
@@ -0,0 +1,150 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Syndicate RENTV — CRE News & Deal Feeds</title>
+<meta name="description" content="Put RENTV's Western-U.S. commercial real estate news and deal feed anywhere — RSS and JSON Feed for your reader, site or newsletter tool.">
+<link rel="alternate" type="application/rss+xml" title="RENTV News" href="/feed.xml">
+<link rel="alternate" type="application/json" title="RENTV News (JSON Feed)" href="/feed.json">
+<link rel="alternate" type="application/rss+xml" title="RENTV Deals" href="/deals.xml">
+<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,800;1,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
+<style>
+  :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--wash:#f5f6f8;--up:#0f8a4d}
+  *{box-sizing:border-box;margin:0}
+  body{font-family:'Inter',system-ui,sans-serif;color:var(--ink);background:#fbfbfc;line-height:1.45}
+  a{color:inherit;text-decoration:none}
+  .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:54px;padding:0 16px;background:#fff;border-bottom:1px solid var(--line)}
+  .burger{display:inline-flex;align-items:center;gap:10px;border:0;background:none;cursor:pointer;font:inherit;color:inherit;padding:7px 9px;border-radius:8px}
+  .burger:hover{background:rgba(0,0,0,.06)}
+  .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sub)}
+  .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',Georgia,serif;font-weight:800;font-size:25px;letter-spacing:.02em}.brand span{color:var(--red)}
+  .user{width:37px;height:37px;border-radius:50%;border:1.5px solid var(--line);background:#fff;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ink)}
+  .user:hover{border-color:var(--red);color:var(--red)}
+  .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:opacity .22s;z-index:950}
+  .scrim.open{opacity:1;pointer-events:auto}
+  .drawer{position:fixed;top:0;left:0;height:100%;width:360px;max-width:88vw;background:#0b0e12;color:#e7ecf1;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
+  .drawer.open{transform:none}
+  .drawer .dh{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;border-bottom:1px solid #1c232b}
+  .drawer .dh .w{font-family:'Playfair Display',serif;font-size:23px;font-weight:800}.drawer .dh .w span{color:var(--red)}
+  .drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer;line-height:1}
+  .grp{padding:14px 18px 4px}.grp .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;margin-bottom:6px}
+  .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;font-size:14.5px;border-left:3px solid transparent}
+  .drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
+  .drawer nav a.active{background:#141a21;border-left-color:var(--red);color:#fff;font-weight:700}
+  .drawer .df{margin-top:auto;padding:16px 18px;border-top:1px solid #1c232b;font-size:11px;color:#6b747d}
+
+  .wrap{max-width:900px;margin:0 auto;padding:0 22px;width:100%}
+  .shead{padding:34px 0 6px}
+  .shead .k{color:var(--red);font-weight:800;font-size:12px;letter-spacing:.16em;text-transform:uppercase}
+  .shead h1{font-family:'Playfair Display',serif;font-size:clamp(32px,3.8vw,48px);font-weight:700;letter-spacing:-.01em;margin-top:4px}
+  .shead p{color:var(--sub);font-size:15px;margin-top:6px;max-width:620px}
+  .feeds{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;padding:22px 0 6px}
+  @media(max-width:760px){.feeds{grid-template-columns:1fr}}
+  .feed{background:#fff;border:1px solid var(--line);border-radius:13px;padding:18px}
+  .feed .fmt{display:inline-block;font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#fff;background:var(--red);border-radius:20px;padding:3px 10px}
+  .feed .fmt.json{background:#0b0e12}
+  .feed h3{font-family:'Playfair Display',serif;font-size:20px;font-weight:800;margin:11px 0 3px}
+  .feed p{color:var(--sub);font-size:12.5px;min-height:34px}
+  .feed .url{display:flex;gap:6px;margin-top:10px}
+  .feed .url code{flex:1;background:var(--wash);border:1px solid var(--line);border-radius:7px;padding:8px 10px;font:600 11.5px/1.3 ui-monospace,Menlo,monospace;color:#2c3238;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+  .feed .url button{border:1.5px solid var(--line);background:#fff;border-radius:7px;padding:0 12px;font:800 11px 'Inter';cursor:pointer;color:var(--ink)}
+  .feed .url button:hover{border-color:var(--red);color:var(--red)}
+  .note{background:var(--wash);border:1px solid var(--line);border-left:3px solid var(--red);border-radius:8px;padding:12px 15px;margin:16px 0;font-size:13px;color:#3c4249}
+  .note b{color:var(--ink)}
+  .sec{padding:24px 0 60px}
+  .sec .rule{font-size:12px;text-transform:uppercase;letter-spacing:.09em;border-top:2px solid var(--ink);padding-top:9px;margin-bottom:10px;font-weight:800}
+  .pv{display:grid;grid-template-columns:auto 1fr;gap:11px;padding:10px 0;border-bottom:1px solid var(--line);align-items:baseline}
+  .pv .c{font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:#fff;background:#4a4f55;border-radius:20px;padding:3px 8px;white-space:nowrap}
+  .pv h4{font:600 15px/1.3 'Inter'}.pv:hover h4{color:var(--red)}
+  .toast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(20px);background:#0b0e12;color:#fff;padding:11px 20px;border-radius:9px;font-weight:700;font-size:13px;opacity:0;pointer-events:none;transition:.25s;z-index:960}
+  .toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
+</style></head><body>
+
+<header class="top">
+  <button class="burger" id="burger" aria-label="Menu">
+    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
+    <span class="lbl">Menu</span>
+  </button>
+  <a class="brand" href="/">REN<span>TV</span></a>
+  <a class="user" href="/search" aria-label="Search" title="Search">
+    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
+  </a>
+</header>
+
+<div class="scrim" id="scrim"></div>
+<aside class="drawer" id="drawer">
+  <div class="dh"><div class="w">REN<span>TV</span></div><button id="dx" aria-label="Close">&times;</button></div>
+  <div class="grp"><div class="gt">Intelligence Hub</div></div>
+  <nav><a href="/hub">All Tools</a><a href="/search">Search</a><a href="/pulse">Market Pulse</a><a href="/sector">Sector Deep-Dives</a><a href="/metros">Deal Flow by Metro</a><a href="/tools">CRE Toolkit</a><a href="/watchlist">Deal Watchlist</a><a href="/directory">CRE Directory</a><a href="/compare">Deal Comparison</a><a href="/brief">The RENTV Brief</a><a href="/syndicate" class="active">Syndicate</a></nav>
+  <div class="grp"><div class="gt">Sections</div></div>
+  <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a></nav>
+  <div class="grp"><div class="gt">Company</div></div>
+  <nav><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe / Newsletters</a></nav>
+  <div class="df">RENTV.com — Commercial real estate news, Western U.S.</div>
+</aside>
+
+<main class="wrap">
+  <div class="shead">
+    <div class="k">Syndicate</div>
+    <h1>Put the RENTV feed anywhere</h1>
+    <p>Pull RENTV's Western-U.S. commercial real estate headlines and deals into your reader, website, Slack or newsletter tool. Standard RSS and JSON Feed — no account, no scraping.</p>
+  </div>
+
+  <div class="feeds">
+    <div class="feed">
+      <span class="fmt">RSS · News</span>
+      <h3>News feed</h3>
+      <p>The latest CRE stories — sales, leases, financing and development.</p>
+      <div class="url"><code id="u1">/feed.xml</code><button data-c="u1">Copy</button></div>
+    </div>
+    <div class="feed">
+      <span class="fmt json">JSON · News</span>
+      <h3>JSON Feed</h3>
+      <p>Same headlines in JSON Feed 1.1 — ideal for apps and automations.</p>
+      <div class="url"><code id="u2">/feed.json</code><button data-c="u2">Copy</button></div>
+    </div>
+    <div class="feed">
+      <span class="fmt">RSS · Deals</span>
+      <h3>Deals feed</h3>
+      <p>Tracked transactions with type, market and price.</p>
+      <div class="url"><code id="u3">/deals.xml</code><button data-c="u3">Copy</button></div>
+    </div>
+  </div>
+
+  <div class="note" id="gate"></div>
+
+  <div class="sec" id="preview-sec">
+    <div class="rule">Live preview — News feed</div>
+    <div id="preview"><div style="color:var(--sub);font-size:13px;padding:12px 0">Loading the feed…</div></div>
+  </div>
+</main>
+<div class="toast" id="toast"></div>
+
+<script>
+const $=id=>document.getElementById(id);
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const drawer=$('drawer'),scrim=$('scrim');
+$('burger').onclick=()=>{drawer.classList.add('open');scrim.classList.add('open')};
+const close=()=>{drawer.classList.remove('open');scrim.classList.remove('open')};
+$('dx').onclick=close;scrim.onclick=close;document.addEventListener('keydown',e=>{if(e.key==='Escape')close()});
+
+// absolute feed URLs from this origin
+const origin=location.origin;
+['u1','u2','u3'].forEach(id=>{$(id).textContent=origin+$(id).textContent;});
+function toast(m){const t=$('toast');t.textContent=m;t.classList.add('on');setTimeout(()=>t.classList.remove('on'),1800);}
+document.querySelectorAll('.url button').forEach(b=>b.onclick=async()=>{
+  const url=$(b.dataset.c).textContent;
+  try{await navigator.clipboard.writeText(url);toast('Feed URL copied');}catch(e){toast(url);}
+});
+
+$('gate').innerHTML='<b>Heads up:</b> while the site is in preview these feeds share its access gate. At public launch they become openly subscribable — links always open on RENTV, never a third-party site.';
+
+(async function(){
+  try{
+    const d=await (await fetch('/feed.json')).json();
+    const items=(d.items||[]).slice(0,8);
+    $('preview').innerHTML=items.map(it=>`<a class="pv" href="${esc(it.url)}"><span class="c">${esc((it.tags&&it.tags[0])||'News')}</span><h4>${esc(it.title)}</h4></a>`).join('')
+      +`<div style="color:var(--sub);font-size:12px;padding:12px 0 0">Showing ${items.length} of ${(d.items||[]).length} — full feed at <code style="font-size:11px">${esc(origin)}/feed.json</code></div>`;
+  }catch(e){ $('preview').innerHTML='<div style="color:var(--sub);font-size:13px">Feed preview unavailable.</div>'; }
+})();
+</script>
+</body></html>
diff --git a/server.js b/server.js
index a1c72c88..914ee7eb 100644
--- a/server.js
+++ b/server.js
@@ -486,5 +486,51 @@ app.get('/api/pulse', (_req, res) => {
   });
 });
 
+// ── Syndication feeds — RSS 2.0 + JSON Feed for news, RSS for deals. ──
+// Links are absolute-to-OUR-host and internal (/article.html, /deals) — never
+// rentv.com. Currently behind the site auth gate like everything else; to make
+// them publicly subscribable at launch, move these 3 routes ABOVE the auth
+// middleware (they read only already-public headlines).
+const xmlEsc = (s) => String(s == null ? '' : s).replace(/[<>&'"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' }[c]));
+const baseUrl = (req) => `${req.headers['x-forwarded-proto'] || req.protocol}://${req.get('host')}`;
+
+app.get('/feed.xml', (req, res) => {
+  const B = baseUrl(req);
+  const items = (readJSON('news.json', {}).items || []).slice(0, 40);
+  const body = items.map((n) => {
+    const link = `${B}/article.html?id=${encodeURIComponent(n.id)}`;
+    return `    <item>\n      <title>${xmlEsc(n.title)}</title>\n      <link>${xmlEsc(link)}</link>\n      <guid isPermaLink="false">rentv-${xmlEsc(n.id)}</guid>\n      ${n.cat ? `<category>${xmlEsc(n.cat)}</category>` : ''}\n    </item>`;
+  }).join('\n');
+  res.type('application/rss+xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0"><channel>\n  <title>RENTV — Commercial Real Estate News</title>\n  <link>${B}/</link>\n  <description>Commercial real estate news, deals and financing across the Western U.S.</description>\n  <language>en-us</language>\n${body}\n</channel></rss>\n`);
+});
+
+app.get('/feed.json', (req, res) => {
+  const B = baseUrl(req);
+  const items = (readJSON('news.json', {}).items || []).slice(0, 40).map((n) => ({
+    id: `rentv-${n.id}`,
+    title: n.title,
+    url: `${B}/article.html?id=${encodeURIComponent(n.id)}`,
+    tags: n.cat ? [n.cat] : undefined,
+  }));
+  res.type('application/json').send(JSON.stringify({
+    version: 'https://jsonfeed.org/version/1.1',
+    title: 'RENTV — Commercial Real Estate News',
+    home_page_url: `${B}/`,
+    feed_url: `${B}/feed.json`,
+    description: 'Commercial real estate news across the Western U.S.',
+    items,
+  }, null, 2));
+});
+
+app.get('/deals.xml', (req, res) => {
+  const B = baseUrl(req);
+  const items = (readJSON('deals.json', {}).deals || []).slice(0, 40);
+  const body = items.map((d) => {
+    const desc = [d.property_type, [d.city, d.state].filter(Boolean).join(', '), d.amount_label].filter(Boolean).join(' · ');
+    return `    <item>\n      <title>${xmlEsc(d.title)}</title>\n      <link>${xmlEsc(B + '/deals')}</link>\n      <guid isPermaLink="false">rentv-deal-${xmlEsc(d.id)}</guid>\n      <description>${xmlEsc(desc)}</description>\n      ${d.property_type ? `<category>${xmlEsc(d.property_type)}</category>` : ''}\n    </item>`;
+  }).join('\n');
+  res.type('application/rss+xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0"><channel>\n  <title>RENTV — CRE Deals &amp; Transactions</title>\n  <link>${B}/deals</link>\n  <description>Tracked commercial real estate transactions across the Western U.S.</description>\n  <language>en-us</language>\n${body}\n</channel></rss>\n`);
+});
+
 app.use(express.static(PUB, { extensions: ['html'] }));
 app.listen(PORT, () => console.log('rentv-v1 (full site, gated, live data) on ' + PORT));
diff --git a/tools/specs/syndicate.json b/tools/specs/syndicate.json
new file mode 100644
index 00000000..e1aab450
--- /dev/null
+++ b/tools/specs/syndicate.json
@@ -0,0 +1,23 @@
+{
+  "slug": "syndicate",
+  "title": "Syndicate RENTV",
+  "subtitle": "The CRE feed, anywhere",
+  "voice": "Samantha",
+  "scenes": [
+    {
+      "url": "/syndicate",
+      "say": "Syndicate lets anyone pull RENTV's Western-U.S. commercial real estate feed anywhere — with standard RSS for news, JSON Feed for apps, and a separate deals feed.",
+      "caption": "RSS + JSON feeds — news and deals, no account"
+    },
+    {
+      "url": "/feed.xml",
+      "say": "Each one is a real, standards-compliant feed. Here's the news RSS — ready to drop into any reader, website or newsletter tool.",
+      "caption": "Standard RSS 2.0 — drop it into any reader"
+    },
+    {
+      "url": "/syndicate#preview-sec",
+      "say": "The page previews the latest headlines live and copies any feed URL in one click. And every link always opens on RENTV — never a third-party site.",
+      "caption": "Live preview · one-click copy · links stay on RENTV"
+    }
+  ]
+}

← 52ea90e7 tools: full 10-scene walkthrough spec (Hub + all 9 tools gui  ·  back to Rentv  ·  auto-save: 2026-07-24T07:56:29 (3 files) — data/markets.json 73b58020 →