← back to Rentv
Fold The REview (rentvreview.com) into rentv-v1 as native video section
f3c51020da67452ada9a728a3d1f33f5a248d6e0 · 2026-07-20 11:48:42 -0700 · Steve Abrams
- pull-videos.mjs: $0 live harvest of rentvreview.com catalog -> data/videos.json
- /api/videos feed + /review route in server.js
- public/review.html: native RENTV design-system video platform (rails, search, watch modal, post/advertise CTAs)
- The REview added to homepage nav (one unified site)
Per /dtd verdict: Option B (native rebuild, ingest real catalog as seed).
Files touched
M data/news.jsonA data/videos.jsonM public/index.htmlA public/review.htmlA scripts/pull-videos.mjsM server.js
Diff
commit f3c51020da67452ada9a728a3d1f33f5a248d6e0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 20 11:48:42 2026 -0700
Fold The REview (rentvreview.com) into rentv-v1 as native video section
- pull-videos.mjs: $0 live harvest of rentvreview.com catalog -> data/videos.json
- /api/videos feed + /review route in server.js
- public/review.html: native RENTV design-system video platform (rails, search, watch modal, post/advertise CTAs)
- The REview added to homepage nav (one unified site)
Per /dtd verdict: Option B (native rebuild, ingest real catalog as seed).
---
data/news.json | 2 +-
data/videos.json | 144 ++++++++++++++++++++++++++++++++++
public/index.html | 2 +-
public/review.html | 204 ++++++++++++++++++++++++++++++++++++++++++++++++
scripts/pull-videos.mjs | 76 ++++++++++++++++++
server.js | 8 ++
6 files changed, 434 insertions(+), 2 deletions(-)
diff --git a/data/news.json b/data/news.json
index 35e83e68..3cd88c27 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-07-20T18:22:16.178Z",
+ "fetched_at": "2026-07-20T18:42:16.795Z",
"count": 30,
"items": [
{
diff --git a/data/videos.json b/data/videos.json
new file mode 100644
index 00000000..61c42ca2
--- /dev/null
+++ b/data/videos.json
@@ -0,0 +1,144 @@
+{
+ "source": "rentvreview.com (The REview) — live",
+ "fetched_at": "2026-07-20T18:46:15.717Z",
+ "count": 13,
+ "cats": [
+ "JLL TV",
+ "Most Recent",
+ "Industry Events",
+ "Featured",
+ "Most Viewed"
+ ],
+ "items": [
+ {
+ "id": "2898",
+ "yt": "79WFDGZYos4",
+ "title": "JLL’s Supply Chain & Logistics Solutions Overview",
+ "desc": "We are at the forefront of supply chain trends, focused on delivering top-down supply chain strategy and ground-up execution to help you save time, reduce costs, improve services and minimize risk.
When it comes to recognizing the ",
+ "cat": "JLL TV",
+ "view_url": "https://www.rentvreview.com/Videos/View/2898",
+ "thumb": "https://img.youtube.com/vi/79WFDGZYos4/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/79WFDGZYos4"
+ },
+ {
+ "id": "3302",
+ "yt": "xKYEQkUG5zM",
+ "title": "Commercial Real Estate Tech & AI 2026 with Helen Calvin",
+ "desc": "Adoption and investment in tech and AI have accelerated in the commercial real estate industry. Finally, right? How should you pick the right tech, the right system, the right provider? When picking a broker to sell your property should you",
+ "cat": "Most Recent",
+ "view_url": "https://www.rentvreview.com/Videos/View/3302",
+ "thumb": "https://img.youtube.com/vi/xKYEQkUG5zM/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/xKYEQkUG5zM"
+ },
+ {
+ "id": "3273",
+ "yt": "eC-oSUBMjIc",
+ "title": "ICSC Southern California Kickoff Panel",
+ "desc": "Commercial real estate is a constantly evolving industry, and staying ahead of the curve is crucial for success. Recently, a panel discussion at the Hyatt Regency Huntington Beach Resort & Spa brought together leading developers to share th",
+ "cat": "Industry Events",
+ "view_url": "https://www.rentvreview.com/Videos/View/3273",
+ "thumb": "https://img.youtube.com/vi/eC-oSUBMjIc/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/eC-oSUBMjIc"
+ },
+ {
+ "id": "3301",
+ "yt": "O6Mh2VmXGzg",
+ "title": "Office Update with Yardi",
+ "desc": "In this episode of America’s Commercial Real Estate Show, host Michael Bull is joined by Doug Ressler and Peter Kolaczynski from Yardi Matrix to break down the evolving office market in 2026.

They explore key trends in of",
+ "cat": "Most Recent",
+ "view_url": "https://www.rentvreview.com/Videos/View/3301",
+ "thumb": "https://img.youtube.com/vi/O6Mh2VmXGzg/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/O6Mh2VmXGzg"
+ },
+ {
+ "id": "3300",
+ "yt": "Wm_dhTl8gn8",
+ "title": "Single Tenant Cap Rates and Market Intel with Nancy Miller, CCIM",
+ "desc": "Nancy Miller with Bull Realty joins Michael to discuss the latest cap rates, loan rates, and expanding tenants in the single tenant net lease investment sector. Discussions include market intel, cap rates, mistakes to avoid, and investor st",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/3300",
+ "thumb": "https://img.youtube.com/vi/Wm_dhTl8gn8/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/Wm_dhTl8gn8"
+ },
+ {
+ "id": "3299",
+ "yt": "6EJEWbXFQ-c",
+ "title": "People Live in Homes Not Corporations Housing Bill Update with Mitch Roschelle",
+ "desc": "The 21st Century ROAD to Housing Act (HR6644) passed by the Senate on March 12th, aims to increase U.S housing supply, reduce costs and modernize financing representing a major federal investment in housing. Part of the bill, with some carv",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/3299",
+ "thumb": "https://img.youtube.com/vi/6EJEWbXFQ-c/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/6EJEWbXFQ-c"
+ },
+ {
+ "id": "3298",
+ "yt": "_1W6U47VN6g",
+ "title": "U.S. Hotel Performance Forecast with Ryan Meliker",
+ "desc": "As world events unfold and America first implications impact the economy and travel, how might U.S. hotels perform moving forward? What cities hold promise for investors and developers? Join show host and commercial broker Michael Bull, CCI",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/3298",
+ "thumb": "https://img.youtube.com/vi/_1W6U47VN6g/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/_1W6U47VN6g"
+ },
+ {
+ "id": "1034",
+ "yt": "ql3qVQNRQJ8",
+ "title": "LoopNet vs Crexi: Which is Better for Net Lease Property Sales?",
+ "desc": "In this video, we take a deep dive into the comparisons between the leading commercial real estate marketplace platforms, LoopNet and Crexi.

SUBSCRIBE to our newsletter to have the top stories hand-delivered to your inbox",
+ "cat": "Most Viewed",
+ "view_url": "https://www.rentvreview.com/Videos/View/1034",
+ "thumb": "https://img.youtube.com/vi/ql3qVQNRQJ8/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/ql3qVQNRQJ8"
+ },
+ {
+ "id": "1650",
+ "yt": "-_QFDzl48tU",
+ "title": "1031 Exchange Explained: 45 Day Rule Checklist",
+ "desc": "Looking to invest in real estate and want the 1031 exchange explained? Make sure you understand the 1031 exchange rule with this free 45 day rule checklist! 

The 1031 exchange rule is a complicated but important process f",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/1650",
+ "thumb": "https://img.youtube.com/vi/-_QFDzl48tU/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/-_QFDzl48tU"
+ },
+ {
+ "id": "3261",
+ "yt": "v8CaeZHpTMM",
+ "title": "VeganBurg Now Open at High Desert Gateway",
+ "desc": "VeganBurg Now Open at High Desert Gateway
",
+ "cat": "Industry Events",
+ "view_url": "https://www.rentvreview.com/Videos/View/3261",
+ "thumb": "https://img.youtube.com/vi/v8CaeZHpTMM/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/v8CaeZHpTMM"
+ },
+ {
+ "id": "3237",
+ "yt": "ujVQ-m0gEpw",
+ "title": "Stater Bros. Markets Grand Opening Celebration at The Collection",
+ "desc": "Stater Bros. Markets Grand Opening Celebration at The Collection
",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/3237",
+ "thumb": "https://img.youtube.com/vi/ujVQ-m0gEpw/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/ujVQ-m0gEpw"
+ },
+ {
+ "id": "3230",
+ "yt": "8UtGSxHYQXY",
+ "title": "Project Spotlight: The Commons at Meridian",
+ "desc": "Project Spotlight: The Commons at Meridian
",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/3230",
+ "thumb": "https://img.youtube.com/vi/8UtGSxHYQXY/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/8UtGSxHYQXY"
+ },
+ {
+ "id": "3229",
+ "yt": "HzCoIw71Cv8",
+ "title": "ICSC & The Western Conference 2024 with Vicky Hammond",
+ "desc": "ICSC & The Western Conference with Vicky Hammond 2024
",
+ "cat": "Featured",
+ "view_url": "https://www.rentvreview.com/Videos/View/3229",
+ "thumb": "https://img.youtube.com/vi/HzCoIw71Cv8/hqdefault.jpg",
+ "embed": "https://www.youtube.com/embed/HzCoIw71Cv8"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index bfa75a7f..21e03259 100644
--- a/public/index.html
+++ b/public/index.html
@@ -82,7 +82,7 @@
</div></header>
<nav><div class="wrap" id="nav">
- <a data-cat="all" class="on">Home</a><a data-cat="Sales">Sales</a><a data-cat="Leases">Leases</a><a data-cat="Financing">Financing</a><a data-cat="Development">Development</a><a data-cat="Multifamily">Multifamily</a><a data-cat="Industrial">Industrial</a><a data-cat="Retail">Retail</a><a href="#conf">Conferences</a>
+ <a data-cat="all" class="on">Home</a><a data-cat="Sales">Sales</a><a data-cat="Leases">Leases</a><a data-cat="Financing">Financing</a><a data-cat="Development">Development</a><a data-cat="Multifamily">Multifamily</a><a data-cat="Industrial">Industrial</a><a data-cat="Retail">Retail</a><a href="/review">The REview</a><a href="#conf">Conferences</a>
</div></nav>
<div class="breaking"><div class="wrap">
diff --git a/public/review.html b/public/review.html
new file mode 100644
index 00000000..2e81c91b
--- /dev/null
+++ b/public/review.html
@@ -0,0 +1,204 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>The REview — RENTV.com Commercial Real Estate Video</title>
+<meta name="description" content="The REview by RENTV.com — free commercial real estate video: market analysis, technology, property showcases and industry events across the Western U.S.">
+<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,400;0,700;1,400&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
+<style>
+ /* ── shared RENTV design system (same tokens as index.html) ── */
+ :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--sans:'Inter',system-ui,sans-serif;--serif:'Playfair Display',Georgia,serif}
+ *{box-sizing:border-box;margin:0;padding:0}
+ body{font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.45}
+ a{color:inherit;text-decoration:none}
+ .wrap{max-width:1200px;margin:0 auto;padding:0 20px}
+ .util{background:var(--ink);color:#cfd4d9;font-size:12px}
+ .util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}
+ .util .left a{color:#cfd4d9;margin-right:16px}
+ .util .acts a{margin-left:14px;color:#cfd4d9}.util .acts a.sub{color:#fff;background:var(--red);padding:4px 12px;border-radius:3px;font-weight:700}
+ .mast{border-bottom:2px solid var(--ink);padding:18px 0 12px}
+ .mast .wrap{display:flex;align-items:center;justify-content:space-between}
+ .logo{font-family:var(--serif);font-weight:700;font-size:34px;letter-spacing:-1px}.logo span{color:var(--red)}
+ .logo small{display:block;font-size:11px;font-weight:600;letter-spacing:3px;color:var(--sub);text-transform:uppercase;margin-top:2px;font-family:var(--sans)}
+ .datebox{font-size:12px;color:var(--sub);text-align:right}
+ nav{border-bottom:1px solid var(--line);position:sticky;top:0;background:#fff;z-index:10}
+ nav .wrap{display:flex;gap:2px;align-items:center;height:46px;overflow-x:auto}
+ nav a{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:14px 13px;border-bottom:3px solid transparent;white-space:nowrap;cursor:pointer}
+ nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
+ /* ── REview hero band ── */
+ .rhero{background:linear-gradient(120deg,#111827,#c8102e);color:#fff;padding:30px 0}
+ .rhero .wrap{display:flex;align-items:center;justify-content:space-between;gap:24px;flex-wrap:wrap}
+ .rhero .k{font-size:11px;letter-spacing:.24em;text-transform:uppercase;opacity:.85}
+ .rhero h1{font-family:var(--serif);font-size:34px;line-height:1.1;margin:6px 0 6px}
+ .rhero p{font-size:14px;opacity:.92;max-width:56ch}
+ .rhero .cta{display:flex;gap:10px;flex:none}
+ .rhero .cta a{display:inline-block;font-weight:800;font-size:12px;text-transform:uppercase;letter-spacing:.06em;padding:11px 18px;border-radius:4px}
+ .rhero .cta .p{background:#fff;color:var(--red)}.rhero .cta .g{border:1.5px solid rgba(255,255,255,.6);color:#fff}
+ /* ── search + filter bar ── */
+ .toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:22px 0 6px}
+ .toolbar input{flex:1;min-width:220px;padding:11px 14px;border:1px solid var(--line);border-radius:5px;font-size:14px;font-family:var(--sans)}
+ .chips{display:flex;gap:8px;flex-wrap:wrap;padding:8px 0 4px}
+ .chips button{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:7px 13px;border:1px solid var(--line);background:#fff;color:var(--sub);border-radius:20px;cursor:pointer}
+ .chips button.on{background:var(--red);border-color:var(--red);color:#fff}
+ .live{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--sub);letter-spacing:.06em;text-transform:uppercase;margin-left:auto}
+ .live .dot{width:7px;height:7px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.6)}70%{box-shadow:0 0 0 7px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
+ /* ── rails + cards ── */
+ .rail{padding:14px 0 6px}
+ .sechead{display:flex;align-items:center;gap:12px;border-top:3px solid var(--ink);padding-top:10px;margin:22px 0 16px}
+ .sechead h2{font-size:16px;text-transform:uppercase;letter-spacing:.06em}
+ .sechead .count{margin-left:auto;font-size:12px;color:var(--sub)}
+ .cards{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:22px}
+ .vcard{display:block;cursor:pointer;text-align:left;background:none;border:0;padding:0;font:inherit;color:inherit}
+ .vcard .thumb{aspect-ratio:16/9;border-radius:6px;overflow:hidden;background:linear-gradient(135deg,#475569,#94a3b8);position:relative}
+ .vcard .thumb img{width:100%;height:100%;object-fit:cover;display:block}
+ .vcard .thumb::after{content:'▶';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:34px;text-shadow:0 2px 12px rgba(0,0,0,.5);opacity:.92;transition:transform .15s}
+ .vcard:hover .thumb::after{transform:scale(1.18)}
+ .vcard .rcat{display:inline-block;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--red);margin:9px 0 3px}
+ .vcard h4{font-family:var(--serif);font-size:16px;line-height:1.22}
+ .vcard .d{font-size:12px;color:var(--sub);margin-top:5px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
+ .empty{color:var(--sub);padding:26px 0}
+ /* ── watch modal ── */
+ .modal{position:fixed;inset:0;background:rgba(10,12,14,.86);display:none;align-items:center;justify-content:center;z-index:50;padding:20px}
+ .modal.open{display:flex}
+ .modal .panel{background:#0d0f11;border-radius:10px;overflow:hidden;max-width:960px;width:100%}
+ .modal .video{aspect-ratio:16/9;background:#000}.modal iframe{width:100%;height:100%;border:0}
+ .modal .meta{padding:16px 20px;color:#e7ebee}
+ .modal .meta .rcat{color:#ff6b81;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
+ .modal .meta h3{font-family:var(--serif);font-size:22px;margin:6px 0 8px}
+ .modal .meta p{font-size:13px;color:#aeb6bd}
+ .modal .meta .row{display:flex;gap:16px;align-items:center;margin-top:12px}
+ .modal .meta a{font-size:12px;font-weight:700;color:#ff6b81}
+ .modal .x{position:absolute;top:16px;right:22px;color:#fff;font-size:30px;cursor:pointer;line-height:1}
+ footer{background:var(--ink);color:#aeb6bd;margin-top:36px;padding:40px 0 30px;font-size:13px}
+ footer .cols{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:28px}
+ footer .logo{color:#fff;font-size:26px;margin-bottom:8px}footer h5{color:#fff;font-size:12px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}footer a{display:block;padding:4px 0;color:#aeb6bd}
+ footer .fine{border-top:1px solid #2a2f34;margin-top:26px;padding-top:16px;font-size:12px;color:#7c848b}
+ @media(max-width:980px){.cards{grid-template-columns:1fr 1fr 1fr}}
+ @media(max-width:720px){.cards{grid-template-columns:1fr 1fr}.rhero h1{font-size:26px}}
+</style>
+</head>
+<body>
+ <div class="util"><div class="wrap">
+ <div class="left"><a href="/">RENTV.com News</a><a href="/review">The REview</a><a href="https://sublease.com/" target="_blank" rel="noopener">Sublease.com</a></div>
+ <div class="acts"><a href="https://www.rentvreview.com/Account/Login" target="_blank" rel="noopener">Log In</a><a href="https://www.rentvreview.com/ContactUs" target="_blank" rel="noopener">Advertise</a><a class="sub" href="https://www.rentvreview.com/Account/Register" target="_blank" rel="noopener">Post a Video</a></div>
+ </div></div>
+
+ <header class="mast"><div class="wrap">
+ <div class="logo">REN<span>TV</span>.com<small>Commercial Real Estate</small></div>
+ <div class="datebox"><span>The REview · CRE Video</span><br><span id="today"></span></div>
+ </div></header>
+
+ <nav><div class="wrap">
+ <a href="/">Home</a><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><a class="on" href="/review">The REview</a>
+ </div></nav>
+
+ <section class="rhero"><div class="wrap">
+ <div>
+ <div class="k">The REview · by RENTV.com</div>
+ <h1>Commercial Real Estate, on video.</h1>
+ <p>Market analysis, technology, property showcases and industry-event coverage — the free CRE video hub from RENTV.com. Post your own, or advertise to the industry.</p>
+ </div>
+ <div class="cta">
+ <a class="p" href="https://www.rentvreview.com/Account/Register" target="_blank" rel="noopener">Post a Video</a>
+ <a class="g" href="https://www.rentvreview.com/ContactUs" target="_blank" rel="noopener">Advertise</a>
+ </div>
+ </div></section>
+
+ <div class="wrap">
+ <div class="toolbar">
+ <input id="q" placeholder="Search The REview — companies, topics, markets…" autocomplete="off">
+ <span class="live"><span class="dot"></span><span id="freshness">live</span></span>
+ </div>
+ <div class="chips" id="chips"></div>
+ <div id="rails"><div class="empty">Loading The REview video catalog…</div></div>
+ </div>
+
+ <footer><div class="wrap"><div class="cols">
+ <div><div class="logo">REN<span style="color:var(--red)">TV</span>.com</div><p style="max-width:36ch">The REview — free commercial real estate video from RENTV.com. Serving the Western U.S. CRE community since 1998.</p></div>
+ <div><h5>The REview</h5><a href="/review">All Video</a><a href="https://www.rentvreview.com/Search" target="_blank" rel="noopener">Search</a><a href="https://www.rentvreview.com/Instructions" target="_blank" rel="noopener">How to Post</a></div>
+ <div><h5>RENTV Network</h5><a href="/">RENTV.com News</a><a href="https://sublease.com/" target="_blank" rel="noopener">Sublease.com</a><a href="https://www.rentvreview.com/ContactUs" target="_blank" rel="noopener">Advertise</a></div>
+ <div><h5>Company</h5><a href="https://www.rentvreview.com/ContactUs" target="_blank" rel="noopener">Contact</a><a href="https://www.rentvreview.com/Terms" target="_blank" rel="noopener">Terms</a><a href="https://www.rentvreview.com/Privacy" target="_blank" rel="noopener">Privacy</a></div>
+ </div><div class="fine">© 2026 RENTV.com · The REview video catalog synced live from rentvreview.com · <span id="src"></span></div></div></footer>
+
+ <div class="modal" id="modal">
+ <span class="x" id="mx">×</span>
+ <div class="panel">
+ <div class="video"><iframe id="mframe" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" allowfullscreen></iframe></div>
+ <div class="meta"><span class="rcat" id="mcat"></span><h3 id="mtitle"></h3><p id="mdesc"></p>
+ <div class="row"><a id="mlink" target="_blank" rel="noopener">Open on The REview ↗</a></div>
+ </div>
+ </div>
+ </div>
+
+<script>
+document.getElementById('today').textContent = new Date().toLocaleDateString(undefined,{weekday:'long',month:'long',day:'numeric',year:'numeric'});
+let ALL=[], CATS=[], cat='all', q='';
+const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+
+function chips(){
+ const all=['all',...CATS];
+ document.getElementById('chips').innerHTML=all.map(c=>
+ '<button data-cat="'+esc(c)+'"'+(c===cat?' class="on"':'')+'>'+(c==='all'?'All Video':esc(c))+'</button>').join('');
+}
+function match(v){
+ if(cat!=='all' && v.cat!==cat) return false;
+ if(q){const t=(v.title+' '+v.desc+' '+v.cat).toLowerCase(); if(!t.includes(q)) return false;}
+ return true;
+}
+function cardHTML(v){
+ return '<button class="vcard" data-id="'+esc(v.id)+'">'
+ +'<div class="thumb"><img loading="lazy" src="'+esc(v.thumb)+'" alt="'+esc(v.title)+'" onerror="this.style.display=\'none\'"></div>'
+ +'<span class="rcat">'+esc(v.cat)+'</span><h4>'+esc(v.title)+'</h4>'
+ +(v.desc?'<div class="d">'+esc(v.desc)+'</div>':'')+'</button>';
+}
+function render(){
+ const box=document.getElementById('rails');
+ const items=ALL.filter(match);
+ if(!items.length){box.innerHTML='<div class="empty">No videos match — try another search or category.</div>';return;}
+ // when browsing "all" with no query, group by rail; otherwise one flat result grid
+ if(cat==='all' && !q){
+ box.innerHTML=CATS.map(c=>{
+ const list=ALL.filter(v=>v.cat===c);
+ if(!list.length) return '';
+ return '<section class="rail"><div class="sechead"><h2>'+esc(c)+'</h2><span class="count">'+list.length+' video'+(list.length>1?'s':'')+'</span></div><div class="cards">'+list.map(cardHTML).join('')+'</div></section>';
+ }).join('');
+ }else{
+ box.innerHTML='<section class="rail"><div class="sechead"><h2>'+(q?'Results':esc(cat))+'</h2><span class="count">'+items.length+' video'+(items.length>1?'s':'')+'</span></div><div class="cards">'+items.map(cardHTML).join('')+'</div></section>';
+ }
+}
+function openVideo(id){
+ const v=ALL.find(x=>x.id===id); if(!v) return;
+ document.getElementById('mframe').src=v.embed+'?autoplay=1&rel=0';
+ document.getElementById('mcat').textContent=v.cat;
+ document.getElementById('mtitle').textContent=v.title;
+ document.getElementById('mdesc').textContent=v.desc||'';
+ document.getElementById('mlink').href=v.view_url;
+ document.getElementById('modal').classList.add('open');
+}
+function closeVideo(){document.getElementById('modal').classList.remove('open');document.getElementById('mframe').src='';}
+
+document.getElementById('rails').addEventListener('click',e=>{const b=e.target.closest('.vcard');if(b)openVideo(b.dataset.id);});
+document.getElementById('chips').addEventListener('click',e=>{const b=e.target.closest('button[data-cat]');if(!b)return;cat=b.dataset.cat;chips();render();});
+document.getElementById('q').addEventListener('input',e=>{q=e.target.value.trim().toLowerCase();render();});
+document.getElementById('mx').onclick=closeVideo;
+document.getElementById('modal').addEventListener('click',e=>{if(e.target.id==='modal')closeVideo();});
+document.addEventListener('keydown',e=>{if(e.key==='Escape')closeVideo();});
+
+async function load(){
+ try{
+ const d=await (await fetch('/api/videos')).json();
+ ALL=d.items||[]; CATS=d.cats||[...new Set(ALL.map(v=>v.cat))];
+ chips(); render();
+ if(d.fetched_at){
+ const mins=Math.max(0,Math.round((Date.now()-new Date(d.fetched_at))/60000));
+ document.getElementById('freshness').textContent=mins<2?'synced just now':('synced '+mins+' min ago');
+ document.getElementById('src').textContent='last sync '+new Date(d.fetched_at).toLocaleTimeString();
+ }
+ }catch(e){document.getElementById('rails').innerHTML='<div class="empty">The REview catalog is temporarily unavailable — retrying…</div>';}
+}
+load(); setInterval(load, 300000); // re-sync every 5 min
+</script>
+</body>
+</html>
diff --git a/scripts/pull-videos.mjs b/scripts/pull-videos.mjs
new file mode 100644
index 00000000..6cc86daf
--- /dev/null
+++ b/scripts/pull-videos.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+// Live-data engine for The REview (rentv-v1 video section): pulls the REAL
+// rentvreview.com video catalog on a schedule and writes data/videos.json,
+// which /api/videos serves and public/review.html renders in the RENTV shell.
+// $0 (plain fetch, no browser). Mirrors pull-news.mjs exactly.
+import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+const HERE = dirname(fileURLToPath(import.meta.url));
+const OUT = join(HERE, '..', 'data'); mkdirSync(OUT, { recursive: true });
+const SRC = 'https://www.rentvreview.com';
+
+async function fetchDecoded(url) {
+ const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 RENTV-v1' }, signal: AbortSignal.timeout(20000) });
+ const buf = Buffer.from(await r.arrayBuffer());
+ let s = buf.toString('utf8');
+ if ((s.match(/�/g) || []).length > 5) s = buf.toString('latin1'); // WIN-1252 fallback
+ return s;
+}
+const clean = (t) => (t || '')
+ .replace(/&/g, '&').replace(/'/g, "'").replace(/�?39;/g, "'").replace(/’/gi, "’")
+ .replace(/‘/gi, "‘").replace(/"/g, '"').replace(/ȁ[cd];/gi, '"')
+ .replace(/�?D;/gi, ' ').replace(/�?A;/gi, ' ').replace(/[03];/g, ' ')
+ .replace(/ /g, ' ').replace(/&/g, '&').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+
+// Walk the homepage, splitting on each preview block, and attach the nearest
+// preceding <h4> heading (JLL TV / Most Recent / Most Viewed / Industry Events…)
+// as the video's category rail.
+function parse(html) {
+ const seen = new Set(), items = [];
+ const blocks = html.split('video-preview__thumbnail');
+ for (let i = 1; i < blocks.length; i++) {
+ const before = blocks[i - 1], b = blocks[i];
+ const view = b.match(/\/Videos\/View\/(\d+)/);
+ const yt = b.match(/youtube\.com\/vi\/([A-Za-z0-9_-]{6,})\//i);
+ const title = b.match(/video-preview__text__title[^>]*>[\s\S]*?<div>([\s\S]*?)<\/div>/i);
+ const desc = b.match(/video-preview__text__description[^>]*>([\s\S]*?)<\/div>/i);
+ if (!view || !yt) continue;
+ const id = view[1]; if (seen.has(id)) continue; seen.add(id);
+ // nearest preceding <h4> = the rail this card sits under
+ const heads = [...before.matchAll(/<h4[^>]*>([\s\S]*?)<\/h4>/gi)];
+ const cat = heads.length ? clean(heads[heads.length - 1][1]) : 'Featured';
+ items.push({
+ id,
+ yt: yt[1],
+ title: clean(title ? title[1] : '') || 'RENTV Video',
+ desc: clean(desc ? desc[1] : '').slice(0, 240),
+ cat: cat || 'Featured',
+ view_url: SRC + '/Videos/View/' + id,
+ thumb: 'https://img.youtube.com/vi/' + yt[1] + '/hqdefault.jpg',
+ embed: 'https://www.youtube.com/embed/' + yt[1],
+ });
+ }
+ return items;
+}
+
+try {
+ const html = await fetchDecoded(SRC + '/');
+ const items = parse(html);
+ if (!items.length) throw new Error('parsed 0 videos — markup may have changed');
+ const cats = [...new Set(items.map(v => v.cat))];
+ const payload = {
+ source: 'rentvreview.com (The REview) — live',
+ fetched_at: new Date().toISOString(),
+ count: items.length,
+ cats,
+ items,
+ };
+ writeFileSync(join(OUT, 'videos.json'), JSON.stringify(payload, null, 1));
+ console.log('OK: pulled ' + items.length + ' videos across ' + cats.length + ' rails: ' + cats.join(', '));
+} catch (e) {
+ console.error('pull-videos failed:', e.message);
+ // never overwrite a good cache with nothing — leave last-good in place
+ try { JSON.parse(readFileSync(join(OUT, 'videos.json'), 'utf8')); console.error('kept last-good videos.json'); } catch {}
+ process.exit(1);
+}
diff --git a/server.js b/server.js
index 1241bd50..e56e25e4 100644
--- a/server.js
+++ b/server.js
@@ -28,6 +28,12 @@ app.get('/api/news', (_q, r) => {
r.json(readJSON('news.json', { items: [] }));
});
+// ── The REview: live CRE video catalog (synced from rentvreview.com by cron) ──
+app.get('/api/videos', (_q, r) => {
+ r.set('Cache-Control', 'public, max-age=300');
+ r.json(readJSON('videos.json', { items: [], cats: [] }));
+});
+
// ── 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;
@@ -83,6 +89,8 @@ app.get('/api/article/:id', async (req, res) => {
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')));
+// The REview — the folded-in video platform, served natively in the RENTV shell
+app.get('/review', (_q, r) => r.sendFile(path.join(PUB, 'review.html')));
app.use(express.static(PUB, { extensions: ['html'] }));
app.listen(PORT, () => console.log('rentv-v1 (full site, gated, live data) on ' + PORT));
← bfda803a auto-save: 2026-07-20T11:31:52 (1 files) — data/news.json
·
back to Rentv
·
chore: lint (esc() XSS-guard on homepage feed), v0.1.0 → v0. f94ea170 →