[object Object]

← back to Rentv

metros: Deal Flow by Metro (/metros) — metros ranked by volume/count with bars + sector-led tags, click-to-expand deals, deep-linkable (?open/?sort); city trusted only if it appears in the deal text, else 'Location unconfirmed' bucket (surfaces upstream pull-deals city mis-tag bug)

d3cb47ffd3b93a36af0a6380e35f6f42aa0fc74e · 2026-07-23 17:22:43 -0700 · Steve

Files touched

Diff

commit d3cb47ffd3b93a36af0a6380e35f6f42aa0fc74e
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 17:22:43 2026 -0700

    metros: Deal Flow by Metro (/metros) — metros ranked by volume/count with bars + sector-led tags, click-to-expand deals, deep-linkable (?open/?sort); city trusted only if it appears in the deal text, else 'Location unconfirmed' bucket (surfaces upstream pull-deals city mis-tag bug)
---
 data/markets.json       |   2 +-
 data/news.json          |  16 ++---
 data/ticker.json        |   2 +-
 public/index.html       |   2 +-
 public/metros.html      | 167 ++++++++++++++++++++++++++++++++++++++++++++++++
 tools/specs/metros.json |  23 +++++++
 6 files changed, 201 insertions(+), 11 deletions(-)

diff --git a/data/markets.json b/data/markets.json
index 3690e011..465ee8ab 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T23:46:07.026Z",
+  "fetched_at": "2026-07-24T00:06:07.934Z",
   "source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
   "curve": [
     {
diff --git a/data/news.json b/data/news.json
index 1fbf10f7..d6c49c84 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,8 +1,15 @@
 {
  "source": "rentv.com — live",
- "fetched_at": "2026-07-23T23:46:06.711Z",
+ "fetched_at": "2026-07-24T00:06:07.597Z",
  "count": 30,
  "items": [
+  {
+   "id": "34245",
+   "title": "Velo3D Leases 289k sf Bay Area Manufacturing Facility",
+   "url": "https://www.rentv.com/content/homepage/mainnews/news/34245",
+   "cat": "Leases",
+   "image": "/img/news/news_20302_711ChallengerStLivermoreKaylaCastroColliersJul212026.jpg"
+  },
   {
    "id": "34238",
    "title": "Gilbert, AZ Multifamily Community Refinanced with New Loan for $51.8 Mil",
@@ -205,13 +212,6 @@
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34171",
    "cat": "Leases",
    "image": "/img/news/news_20236_7733LitchfieldRdGlendaleAZStaceyHershauerJun242026.jpg"
-  },
-  {
-   "id": "34169",
-   "title": "Seattle Area Res Community Refinanced with $82.5 Mil Loan from Mesa West Capital",
-   "url": "https://www.rentv.com/content/homepage/mainnews/news/34169",
-   "cat": "Financing",
-   "image": "/img/news/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg"
   }
  ]
 }
\ No newline at end of file
diff --git a/data/ticker.json b/data/ticker.json
index 483259d5..fcc77560 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T23:46:06.996Z",
+  "fetched_at": "2026-07-24T00:06:07.903Z",
   "la_office_vac": {
     "label": "LA OFFICE VAC",
     "value": "23.1%",
diff --git a/public/index.html b/public/index.html
index bb90c0d7..eb567b82 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="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</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="/search">🔎 Search</a><a href="/pulse">📊 Market Pulse</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>
   <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/metros.html b/public/metros.html
new file mode 100644
index 00000000..69cd6e7d
--- /dev/null
+++ b/public/metros.html
@@ -0,0 +1,167 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>Deal Flow by Metro — RENTV.com</title>
+<meta name="description" content="RENTV deal flow by metro — where commercial real estate capital is moving across the Western U.S., ranked by tracked transaction volume.">
+<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:920px;margin:0 auto;padding:0 22px;width:100%}
+  .mhead{padding:34px 0 6px;display:flex;align-items:flex-end;justify-content:space-between;flex-wrap:wrap;gap:10px}
+  .mhead .k{color:var(--red);font-weight:800;font-size:12px;letter-spacing:.16em;text-transform:uppercase}
+  .mhead h1{font-family:'Playfair Display',serif;font-size:clamp(32px,3.6vw,46px);font-weight:700;letter-spacing:-.01em;margin-top:4px}
+  .mhead p{color:var(--sub);font-size:15px;margin-top:4px}
+  .sortbar{display:flex;align-items:center;gap:8px;padding:16px 0 4px}
+  .sortbar .lbl{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--sub)}
+  .sortbar button{border:1.5px solid var(--line);background:#fff;border-radius:20px;padding:6px 14px;font:700 12.5px 'Inter';color:var(--ink);cursor:pointer}
+  .sortbar button.on{background:var(--red);border-color:var(--red);color:#fff}
+
+  .metros{padding:10px 0 60px}
+  .metro{background:#fff;border:1px solid var(--line);border-radius:12px;margin-bottom:12px;overflow:hidden}
+  .mrow{display:grid;grid-template-columns:34px 1fr auto;gap:14px;align-items:center;padding:16px 18px;cursor:pointer}
+  .mrow:hover{background:var(--wash)}
+  .mrow .rk{font-family:'Playfair Display',serif;font-size:22px;font-weight:800;color:var(--sub);text-align:center}
+  .mrow .main .nm{font-size:17px;font-weight:800}
+  .mrow .main .sub{color:var(--sub);font-size:12.5px;margin-top:3px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
+  .mrow .main .sec{font-size:9px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;background:var(--wash);border:1px solid var(--line);border-radius:20px;padding:2px 8px;color:#4a4f55}
+  .mrow .main .track{height:7px;background:var(--wash);border-radius:6px;overflow:hidden;margin-top:9px;max-width:420px}
+  .mrow .main .fill{height:100%;background:var(--red);border-radius:6px;transition:width .6s cubic-bezier(.4,0,.2,1)}
+  .mrow .r{text-align:right}
+  .mrow .r .vol{font-family:'Playfair Display',serif;font-size:22px;font-weight:800;color:var(--red)}
+  .mrow .r .ct{color:var(--sub);font-size:12px;font-weight:600;margin-top:2px}
+  .mrow .r .chev{color:var(--sub);font-size:12px;margin-top:3px}
+  .deals{border-top:1px solid var(--line);background:#fcfcfd;display:none}
+  .metro.open .deals{display:block}
+  .metro.open .chev{transform:rotate(180deg);display:inline-block}
+  .deal{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:center;padding:11px 18px;border-bottom:1px solid var(--line)}
+  .deal:last-child{border-bottom:0}
+  .deal h4{font:600 14.5px/1.3 'Inter'}
+  .deal .m{color:var(--sub);font-size:11.5px;margin-top:2px}
+  .deal .amt{font-family:'Playfair Display',serif;font-weight:800;font-size:16px;color:var(--red);white-space:nowrap}
+  .deal:hover h4{color:var(--red)}
+  .empty{padding:40px 0;text-align:center;color:var(--sub)}
+</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">Discover</div></div>
+  <nav><a href="/search">Search</a><a href="/pulse">Market Pulse</a><a href="/metros" class="active">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>
+  <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>
+  <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/review">The REview</a><a href="/cre-talk">CRE Talk</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="mhead">
+    <div><div class="k">Deal Flow by Metro</div><h1>Where the capital is moving</h1><p>Tracked transactions grouped by metro — tap any market to see its deals.</p></div>
+  </div>
+  <div class="sortbar"><span class="lbl">Rank by</span><button id="sVol" class="on" data-s="vol">Volume</button><button id="sCnt" data-s="cnt">Deal count</button></div>
+  <section class="metros" id="metros"></section>
+</main>
+
+<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()});
+
+const usd=n=>!n?'—':n>=1e9?('$'+(n/1e9).toFixed(1)+'B'):n>=1e6?('$'+(n/1e6).toFixed(1)+'M'):n>=1e3?('$'+Math.round(n/1e3)+'K'):('$'+Math.round(n));
+let METROS=[], sort='vol';
+
+const UNCONF='Location unconfirmed';
+function build(deals){
+  const m={};
+  deals.forEach(d=>{
+    // Trust the city only if it actually appears in the deal's own text. The
+    // upstream feed sometimes mis-assigns a city (e.g. a firm's HQ) that has
+    // nothing to do with the asset — group those honestly, not under a false metro.
+    const hay=`${d.title||''} ${d.summary||''}`.toLowerCase();
+    const cityOk=d.city && hay.includes(String(d.city).toLowerCase());
+    const key=cityOk ? [d.city,d.state].filter(Boolean).join(', ') : UNCONF;
+    const g=m[key]||(m[key]={name:key,count:0,vol:0,deals:[],secs:{}});
+    g.count++; if(typeof d.amount==='number')g.vol+=d.amount; g.deals.push(d);
+    if(d.property_type)g.secs[d.property_type]=(g.secs[d.property_type]||0)+1;
+  });
+  return Object.values(m).map(g=>{
+    g.top=Object.entries(g.secs).sort((a,b)=>b[1]-a[1])[0];
+    g.deals.sort((a,b)=>(b.amount||0)-(a.amount||0));
+    return g;
+  });
+}
+function render(){
+  const rows=METROS.slice().sort((a,b)=>{
+    if(a.name===UNCONF)return 1; if(b.name===UNCONF)return -1; // unconfirmed always last
+    return sort==='vol' ? (b.vol-a.vol||b.count-a.count) : (b.count-a.count||b.vol-a.vol);
+  });
+  const max=Math.max(...rows.filter(r=>r.name!==UNCONF).map(r=>sort==='vol'?r.vol:r.count),1);
+  if(!rows.length){ $('metros').innerHTML='<div class="empty">No deals to map yet.</div>'; return; }
+  $('metros').innerHTML=rows.map((g,i)=>`
+    <div class="metro" data-k="${esc(g.name)}">
+      <div class="mrow">
+        <div class="rk">${i+1}</div>
+        <div class="main">
+          <div class="nm">${esc(g.name)}</div>
+          <div class="sub">${g.top?`<span class="sec">${esc(g.top[0])} led</span>`:''}<span>${g.count} deal${g.count===1?'':'s'}</span>${g.vol?`<span>· avg ${esc(usd(g.vol/g.deals.filter(d=>d.amount).length))}</span>`:''}</div>
+          <div class="track"><div class="fill" style="width:${Math.max(5,Math.round((sort==='vol'?g.vol:g.count)/max*100))}%"></div></div>
+        </div>
+        <div class="r"><div class="vol">${g.vol?esc(usd(g.vol)):'—'}</div><div class="ct">${g.count} deal${g.count===1?'':'s'}</div><div class="chev">▾ deals</div></div>
+      </div>
+      <div class="deals">${g.deals.map(d=>`<a class="deal" href="/deals"><div><h4>${esc(d.title)}</h4><div class="m">${[d.property_type,d.txn_type].filter(Boolean).map(esc).join(' · ')}</div></div><div class="amt">${esc(d.amount_label||(d.amount?usd(d.amount):'—'))}</div></a>`).join('')}</div>
+    </div>`).join('');
+  $('metros').querySelectorAll('.mrow').forEach(r=>r.onclick=()=>r.parentElement.classList.toggle('open'));
+}
+function setSort(s){ sort=s; $('sVol').classList.toggle('on',s==='vol'); $('sCnt').classList.toggle('on',s==='cnt'); render(); }
+$('sVol').onclick=()=>setSort('vol'); $('sCnt').onclick=()=>setSort('cnt');
+
+(async function(){
+  let deals=[]; try{ deals=((await (await fetch('/api/deals?limit=200')).json()).deals||[]); }catch(e){ $('metros').innerHTML='<div class="empty">Deal data unavailable.</div>'; return; }
+  METROS=build(deals);
+  const p=new URLSearchParams(location.search);
+  if(p.get('sort')==='cnt')sort='cnt';
+  setSort(sort);
+  const openM=p.get('open');
+  if(openM){ const el=$('metros').querySelector(`.metro[data-k="${CSS.escape(openM)}"]`); if(el){el.classList.add('open');el.scrollIntoView({block:'center'});} }
+})();
+</script>
+</body></html>
diff --git a/tools/specs/metros.json b/tools/specs/metros.json
new file mode 100644
index 00000000..7dca2a09
--- /dev/null
+++ b/tools/specs/metros.json
@@ -0,0 +1,23 @@
+{
+  "slug": "metros",
+  "title": "Deal Flow by Metro",
+  "subtitle": "Where the capital is moving",
+  "voice": "Samantha",
+  "scenes": [
+    {
+      "url": "/metros",
+      "say": "Deal Flow by Metro shows you where commercial real estate capital is moving across the Western U.S., ranked by tracked transaction volume.",
+      "caption": "Metros ranked by tracked deal volume"
+    },
+    {
+      "url": "/metros?open=Bay%20Area",
+      "say": "Tap any market — like the Bay Area — to expand it and see every deal behind the number, with the leading sector called out.",
+      "caption": "Tap a metro to see every deal behind the number"
+    },
+    {
+      "url": "/metros?sort=cnt",
+      "say": "Flip the ranking to deal count to spot the busiest markets by activity — and any deals with an unconfirmed location are kept honest in their own bucket, never faked into a metro.",
+      "caption": "Re-rank by count · unconfirmed locations kept honest"
+    }
+  ]
+}

← 592d2d9e brief: The RENTV Brief (/brief) — shareable, print-ready CRE  ·  back to Rentv  ·  fix(deals): correct city mis-assignment — shared parse-locat cbaea83a →