[object Object]

← back to Rentv 2026

directory: searchable CRE Directory (/directory) over live firms (34.7k) + brokers (382k) — tabbed, debounced search, load-more pagination, deep-linkable, info-cards (no outbound links)

5a55695412029741b032d5f92993ad1be4fdba30 · 2026-07-23 16:26:35 -0700 · Steve

Files touched

Diff

commit 5a55695412029741b032d5f92993ad1be4fdba30
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 16:26:35 2026 -0700

    directory: searchable CRE Directory (/directory) over live firms (34.7k) + brokers (382k) — tabbed, debounced search, load-more pagination, deep-linkable, info-cards (no outbound links)
---
 data/markets.json          |   2 +-
 data/news.json             |   2 +-
 data/ticker.json           |   2 +-
 public/directory.html      | 155 +++++++++++++++++++++++++++++++++++++++++++++
 public/index.html          |   2 +-
 tools/specs/directory.json |  23 +++++++
 6 files changed, 182 insertions(+), 4 deletions(-)

diff --git a/data/markets.json b/data/markets.json
index d91e944b..207f87aa 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T23:06:05.090Z",
+  "fetched_at": "2026-07-23T23:26:05.978Z",
   "source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
   "curve": [
     {
diff --git a/data/news.json b/data/news.json
index 79bba80a..ee16121c 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
 {
  "source": "rentv.com — live",
- "fetched_at": "2026-07-23T23:06:04.500Z",
+ "fetched_at": "2026-07-23T23:26:05.660Z",
  "count": 30,
  "items": [
   {
diff --git a/data/ticker.json b/data/ticker.json
index 7e4c20de..53356db1 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T23:06:05.063Z",
+  "fetched_at": "2026-07-23T23:26:05.950Z",
   "la_office_vac": {
     "label": "LA OFFICE VAC",
     "value": "23.1%",
diff --git a/public/directory.html b/public/directory.html
new file mode 100644
index 00000000..ece03254
--- /dev/null
+++ b/public/directory.html
@@ -0,0 +1,155 @@
+<!doctype html><html lang="en"><head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>CRE Directory — RENTV.com</title>
+<meta name="description" content="Search the RENTV commercial real estate directory — thousands of California brokerage firms and licensed brokers by name, city and firm.">
+<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:1080px;margin:0 auto;padding:0 22px;width:100%}
+  .dhead{padding:34px 0 6px}
+  .dhead .k{color:var(--red);font-weight:800;font-size:12px;letter-spacing:.16em;text-transform:uppercase}
+  .dhead h1{font-family:'Playfair Display',serif;font-size:clamp(32px,3.6vw,46px);font-weight:700;letter-spacing:-.01em;margin-top:4px}
+  .dhead p{color:var(--sub);font-size:15px;margin-top:4px}
+  .tabs{display:flex;gap:6px;margin:18px 0 0;border-bottom:1px solid var(--line)}
+  .tabs button{border:0;background:none;font:800 14px 'Inter';color:var(--sub);padding:11px 4px;margin-right:18px;cursor:pointer;border-bottom:3px solid transparent;letter-spacing:.02em}
+  .tabs button.on{color:var(--ink);border-bottom-color:var(--red)}
+  .sbox{position:relative;margin:16px 0 6px}
+  .sbox svg{position:absolute;left:15px;top:50%;transform:translateY(-50%);color:var(--sub)}
+  .sbox input{width:100%;font:600 16px 'Inter';padding:14px 16px 14px 46px;border:2px solid var(--line);border-radius:11px;outline:0;background:#fff}
+  .sbox input:focus{border-color:var(--red)}
+  .meta{color:var(--sub);font-size:12.5px;font-weight:600;min-height:18px;margin:8px 2px}
+  .rows{padding:4px 0 8px}
+  .row{display:grid;grid-template-columns:1fr auto;gap:14px;align-items:center;padding:13px 2px;border-bottom:1px solid var(--line)}
+  .row .nm{font:700 15.5px 'Inter'}
+  .row .sub{color:var(--sub);font-size:12.5px;margin-top:2px}
+  .row .r{text-align:right;white-space:nowrap}
+  .row .badge{display:inline-block;font-size:11px;font-weight:800;color:#fff;background:var(--red);border-radius:20px;padding:3px 10px}
+  .row .lic{color:var(--sub);font-size:11px;margin-top:3px;font-variant-numeric:tabular-nums}
+  .row .st{display:inline-block;font-size:10px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;padding:2px 8px;border-radius:20px;background:var(--wash);color:#4a4f55}
+  .row .st.ok{background:rgba(15,138,77,.12);color:var(--up)}
+  .more{display:block;width:100%;margin:14px 0 60px;padding:13px;border:1.5px solid var(--line);border-radius:10px;background:#fff;font:800 13px 'Inter';color:var(--ink);cursor:pointer}
+  .more:hover{border-color:var(--red);color:var(--red)}
+  .more[disabled]{opacity:.5;cursor:default}
+  .empty{padding:44px 0;text-align:center;color:var(--sub)}
+  .empty h3{font-family:'Playfair Display',serif;font-size:23px;color:var(--ink);margin-bottom:6px}
+  .spin{display:inline-block;width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--red);border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px;margin-right:6px}@keyframes sp{to{transform:rotate(360deg)}}
+</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="/tools">CRE Toolkit</a><a href="/watchlist">Deal Watchlist</a><a href="/directory" class="active">CRE Directory</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="dhead">
+    <div class="k">CRE Directory</div>
+    <h1>Every firm &amp; broker, one search</h1>
+    <p>Thousands of California brokerage firms and licensed brokers — search by name, city or firm.</p>
+  </div>
+  <div class="tabs">
+    <button id="tabFirms" class="on" data-tab="firms">Firms</button>
+    <button id="tabBrokers" data-tab="brokers">Brokers</button>
+  </div>
+  <div class="sbox">
+    <svg width="20" height="20" 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>
+    <input id="q" type="search" autocomplete="off" placeholder="Search firms by name or city…">
+  </div>
+  <div class="meta" id="meta"></div>
+  <section class="rows" id="rows"></section>
+  <button class="more" id="more" style="display:none">Load more</button>
+</main>
+
+<script>
+const $=id=>document.getElementById(id);
+const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
+const cap=s=>String(s||'').toLowerCase().replace(/\b\w/g,c=>c.toUpperCase());
+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 PAGE=25;
+let tab='firms', q='', offset=0, total=0, seq=0;
+
+const p=new URLSearchParams(location.search);
+if(p.get('tab')==='brokers')tab='brokers';
+if(p.get('q'))q=p.get('q');
+$('q').value=q;
+
+function setTab(t){ tab=t; offset=0; $('tabFirms').classList.toggle('on',t==='firms'); $('tabBrokers').classList.toggle('on',t==='brokers'); $('q').placeholder=t==='firms'?'Search firms by name or city…':'Search brokers by name, city or firm…'; sync(); load(true); }
+$('tabFirms').onclick=()=>setTab('firms'); $('tabBrokers').onclick=()=>setTab('brokers');
+
+let timer=null;
+$('q').addEventListener('input',e=>{q=e.target.value.trim();clearTimeout(timer);timer=setTimeout(()=>{offset=0;sync();load(true);},220);});
+
+function sync(){ const u=new URLSearchParams(); u.set('tab',tab); if(q)u.set('q',q); history.replaceState(null,'','/directory?'+u.toString()); }
+
+function firmRow(f){ return `<div class="row"><div><div class="nm">${esc(cap(f.name))}</div><div class="sub">${esc(cap(f.hq_city||f.city||''))}${f.hq_state?', '+esc(f.hq_state):''}</div></div><div class="r">${f.agent_count?`<span class="badge">${(+f.agent_count).toLocaleString()} agents</span>`:''}<div class="lic">DRE #${esc(f.license_no||'—')}</div></div></div>`; }
+function brokerRow(b){ const ok=/active/i.test(b.license_status||''); return `<div class="row"><div><div class="nm">${esc(cap(b.name))}</div><div class="sub">${[cap(b.firm_name||''),cap(b.city||''),b.state_code].filter(Boolean).map(esc).join(' · ')}</div></div><div class="r"><span class="st${ok?' ok':''}">${esc(b.license_status||b.license_type||'Lic')}</span><div class="lic">DRE #${esc(b.license_no||'—')}</div></div></div>`; }
+
+async function load(reset){
+  const my=++seq;
+  if(reset){$('rows').innerHTML='';$('meta').innerHTML='<span class="spin"></span>Searching…';$('more').style.display='none';}
+  const u=new URLSearchParams(); if(q)u.set('q',q); u.set('limit',PAGE); u.set('offset',offset);
+  let d; try{ d=await (await fetch('/api/'+tab+'?'+u.toString())).json(); }catch(e){ if(my===seq){$('meta').textContent='Directory service unavailable — try again.';} return; }
+  if(my!==seq) return;
+  total=d.total||0;
+  const rows=d.rows||[];
+  const html=rows.map(tab==='firms'?firmRow:brokerRow).join('');
+  if(reset)$('rows').innerHTML=html; else $('rows').insertAdjacentHTML('beforeend',html);
+  if(!total){ $('meta').textContent=''; $('rows').innerHTML='<div class="empty"><h3>No '+tab+' found</h3><div>Try a different name or city.</div></div>'; return; }
+  $('meta').innerHTML=`${total.toLocaleString()} ${tab}${q?` matching “${esc(q)}”`:''} · showing ${Math.min(offset+rows.length,total).toLocaleString()}`;
+  offset+=rows.length;
+  $('more').style.display=offset<total?'block':'none';
+}
+$('more').onclick=()=>{ $('more').disabled=true; $('more').textContent='Loading…'; load(false).then(()=>{$('more').disabled=false;$('more').textContent='Load more';}); };
+
+$('tabFirms').classList.toggle('on',tab==='firms'); $('tabBrokers').classList.toggle('on',tab==='brokers');
+$('q').placeholder=tab==='firms'?'Search firms by name or city…':'Search brokers by name, city or firm…';
+load(true);
+</script>
+</body></html>
diff --git a/public/index.html b/public/index.html
index 94c7f76c..f8391444 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></nav>
+  <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></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/tools/specs/directory.json b/tools/specs/directory.json
new file mode 100644
index 00000000..1cb38b40
--- /dev/null
+++ b/tools/specs/directory.json
@@ -0,0 +1,23 @@
+{
+  "slug": "directory",
+  "title": "CRE Directory",
+  "subtitle": "Every firm & broker, one search",
+  "voice": "Samantha",
+  "scenes": [
+    {
+      "url": "/directory",
+      "say": "The RENTV CRE Directory puts thousands of California brokerage firms and licensed brokers just one search away.",
+      "caption": "Thousands of firms & brokers — one search"
+    },
+    {
+      "url": "/directory?tab=firms&q=Marcus",
+      "say": "Look up a firm — like Marcus and Millichap — and see its offices, agent counts and license numbers instantly.",
+      "caption": "Search a firm — offices, agent counts, license #"
+    },
+    {
+      "url": "/directory?tab=brokers&q=Smith",
+      "say": "Or flip to brokers and search thousands of licensed agents by name, city or firm — with live license status right on each result.",
+      "caption": "Flip to brokers — search by name, city or firm"
+    }
+  ]
+}

← 82f30a9a auto-save: 2026-07-23T16:22:30 (3 files) — data/markets.json  ·  back to Rentv 2026  ·  compare: Deal Comparison (/compare) — side-by-side price/$pe 7c7b8412 →