← back to Rentv 2026
Add real CRE deal listings: parse rentv.com transaction stories into structured deal cards (/deals page + /api/deals + pull-deals.mjs)
49078ccf77f44173eb66dbf55de1d8a93c8ec106 · 2026-07-22 21:10:28 -0700 · Steve Abrams
Files touched
A data/deals.jsonM data/markets.jsonM data/news.jsonM data/ticker.jsonA public/deals.htmlM public/index.htmlA scripts/pull-deals.mjsM server.js
Diff
commit 49078ccf77f44173eb66dbf55de1d8a93c8ec106
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 21:10:28 2026 -0700
Add real CRE deal listings: parse rentv.com transaction stories into structured deal cards (/deals page + /api/deals + pull-deals.mjs)
---
data/deals.json | 577 +++++++++++++++++++++++++++++++++++++++++++++++++
data/markets.json | 2 +-
data/news.json | 2 +-
data/ticker.json | 2 +-
public/deals.html | 130 +++++++++++
public/index.html | 2 +-
scripts/pull-deals.mjs | 150 +++++++++++++
server.js | 19 ++
8 files changed, 880 insertions(+), 4 deletions(-)
diff --git a/data/deals.json b/data/deals.json
new file mode 100644
index 00000000..5847be97
--- /dev/null
+++ b/data/deals.json
@@ -0,0 +1,577 @@
+{
+ "source": "rentv.com — live deal listings",
+ "fetched_at": "2026-07-23T04:07:13.798Z",
+ "count": 30,
+ "deals": [
+ {
+ "id": "34238",
+ "title": "Gilbert, AZ Multifamily Community Refinanced with New Loan for $51.8 Mil",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34238",
+ "image": "https://www.rentv.com/images/dynamic/news_20296_4353EElliotRdGilbertAZDarcieGiacchettoJul212026.jpg",
+ "txn_type": "Financing",
+ "property_type": "Multifamily",
+ "city": "Gilbert",
+ "state": "AZ",
+ "amount": 51800000,
+ "amount_label": "$51.8M",
+ "size_label": "236 units · 986 SF",
+ "units": 236,
+ "sqft": 986,
+ "address": "4353 E. Elliot Rd.",
+ "occupancy_pct": 97,
+ "year_built": 2025,
+ "summary": "A joint venture between Phoenix Capital Management and P. B."
+ },
+ {
+ "id": "34236",
+ "title": "The Swig Company Obtains Refi on Office High-Rise in San Franciscos Financial District",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34236",
+ "image": "https://www.rentv.com/images/dynamic/news_20294_220MontgomeryStSFNancyAmaralA2N2Jul162026.jpg",
+ "txn_type": "Financing",
+ "property_type": "Office",
+ "city": "San Francisco",
+ "state": null,
+ "amount": 95000000,
+ "amount_label": "$95.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": "220 Montgomery St",
+ "occupancy_pct": null,
+ "year_built": 1891,
+ "summary": "RENTV. com - Commercial real estate news and property information window."
+ },
+ {
+ "id": "34233",
+ "title": "Tailwind Investment Group Spends $65 Mil on Two Sacramento Res Properties",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34233",
+ "image": "https://www.rentv.com/images/dynamic/news_20291_6633ValleyHiDrSacramentoDavidHulmeTailwindJul182026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Sacramento",
+ "state": null,
+ "amount": 65000000,
+ "amount_label": "$65.0M",
+ "size_label": "419 units",
+ "units": 419,
+ "sqft": null,
+ "address": "4350 Galbrath Dr",
+ "occupancy_pct": 94,
+ "year_built": null,
+ "summary": "Tailwind Investment Group has acquired two multifamily properties totaling 419 units in Sacramento for a combined price of $65 mil. The properties include Bent Tree Apartments (208 units) and California Place Apartments (211 units)."
+ },
+ {
+ "id": "34239",
+ "title": "Fresno Multifamily Sale Works Out to $177k per Unit",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34239",
+ "image": "https://www.rentv.com/images/dynamic/news_20297_585WestSierraAveFresnoDaveEbelingJul212026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Fresno",
+ "state": null,
+ "amount": 177000,
+ "amount_label": "$177,000",
+ "size_label": "248 units · 878 SF",
+ "units": 248,
+ "sqft": 878,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "In a Fresno multifamily trade, Maroa Park Apartments, a 248-unit community, was purchased by a private investor for $44 mil ($177. 4k/unit)."
+ },
+ {
+ "id": "34235",
+ "title": "Retail Portfolio in Downtown Long Beach Fetches $30 Mil",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34235",
+ "image": "https://www.rentv.com/images/dynamic/news_20293_MosiacLongBeachAnneMonaghanJul202026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Retail",
+ "city": "Long Beach",
+ "state": null,
+ "amount": 30000000,
+ "amount_label": "$30.0M",
+ "size_label": "4.8 acres",
+ "units": null,
+ "sqft": null,
+ "address": "50 East 4th St",
+ "occupancy_pct": 78,
+ "year_built": null,
+ "summary": "Mosaic, a six building, 148. 4k sf retail portfolio spanning multiple city blocks in Downtown Long Beach, traded hands in a recent $30 mil ($202/sf transaction."
+ },
+ {
+ "id": "34224",
+ "title": "Shorenstein Buys 77k sf Bay Area Office Building",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34224",
+ "image": "https://www.rentv.com/images/dynamic/news_20283_550AllertonRedwoodCityNickTrombolaNWMKJul162026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Office",
+ "city": "Bay Area",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": 81,
+ "year_built": null,
+ "summary": "Shorenstein has closed on a Bay Area office buy, acquiring 550 Allerton, a 76. 6k sf, Class A property in the heart of Downtown Redwood City."
+ },
+ {
+ "id": "34226",
+ "title": "Carson Distribution Facility Acquired by WareSpace in a $23 Mil Deal",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34226",
+ "image": "https://www.rentv.com/images/dynamic/news_20285_860SandhillAveCarsonElisabethManvilleSmartJul162026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Industrial",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 23000000,
+ "amount_label": "$23.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": "860 Sandhill Ave",
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "A 90. 5k sf, industrial distribution facility in the city of Carson traded hands in a $23 mil ($254/sf) transaction."
+ },
+ {
+ "id": "34218",
+ "title": "Camden Property Trust Buys Gilbert, AZ Res Community in $124 Mil Deal",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34218",
+ "image": "https://www.rentv.com/images/dynamic/news_20278_3150ERayRdGilbertKimHoidalCBREJul132026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Gilbert",
+ "state": "AZ",
+ "amount": 124000000,
+ "amount_label": "$124.0M",
+ "size_label": "320 units",
+ "units": 320,
+ "sqft": null,
+ "address": "3150 E Ray Rd",
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "In a major Arizona multifamily trade, Camden Property Trust acquired The Tyler, a 320-unit multifamily community located in the Agritopia master-planned community in Gilbert. The asset was sold by StreetLights Residential in a deal worth $124."
+ },
+ {
+ "id": "34225",
+ "title": "CentrePoint Properties Spends $30 Mil on Trio of Colorado Retail Assets",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34225",
+ "image": "",
+ "txn_type": "Sale",
+ "property_type": "Retail",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 30000000,
+ "amount_label": "$30.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "CentrePoint Properties has purchased a three-property Colorado retail portfolio, including Cottonwood Square in Parker, Bristol Pointe in Aurora and Foothill Green in Littleton. The assets were sold by Western Centers Inc in a $30."
+ },
+ {
+ "id": "34227",
+ "title": "MG Properties Buys 188-Unit Portland Res Asset",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34227",
+ "image": "https://www.rentv.com/images/dynamic/news_20286_TupeloAlleyPortlandPaulKaseburgMGPropJul162026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Portland",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": "188 units",
+ "units": 188,
+ "sqft": null,
+ "address": "3850 N. Mississippi Ave",
+ "occupancy_pct": null,
+ "year_built": 2009,
+ "summary": "MG Properties has acquired Tupelo Alley, a 188-unit mixed use apartment community in Portland, OR. Located at 3850 N."
+ },
+ {
+ "id": "34216",
+ "title": "Meta Housing Well Underway with 300-Unit Res Community in Woodland Hills",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34216",
+ "image": "https://www.rentv.com/images/dynamic/news_20277_21300WOxnardStWHillsHollyJenvyJul152026.jpg",
+ "txn_type": "Development",
+ "property_type": "Multifamily",
+ "city": "Phoenix",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": "300 units · 400 SF",
+ "units": 300,
+ "sqft": 400,
+ "address": "21300 W. Oxnard St",
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "Meta Housing, with R. D."
+ },
+ {
+ "id": "34214",
+ "title": "Intercontinental Real Estate Corporation Closes $69 Mil Retail Buy in Washington",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34214",
+ "image": "https://www.rentv.com/images/dynamic/news_20274_1402LakeTappsPkwySEAuburnWABruceBeckJul152026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Retail",
+ "city": "Auburn",
+ "state": "WA",
+ "amount": 69000000,
+ "amount_label": "$69.0M",
+ "size_label": "13,000 SF · 12.61 acres",
+ "units": null,
+ "sqft": 13000,
+ "address": "1402 Lake Tapps Pkwy",
+ "occupancy_pct": 100,
+ "year_built": 2002,
+ "summary": "Intercontinental Real Estate Corporation has acquired Lakeland Town Center, a 125. 2k sf grocery-anchored retail center located in the Seattle/Tacoma suburb of Auburn, WA, for $69."
+ },
+ {
+ "id": "34208",
+ "title": "Eagle Partners and The Vistria Group Spend $132 Mil on Garden Grove Mixed-Income Res Community",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34208",
+ "image": "https://www.rentv.com/images/dynamic/news_20267_12091BayportStGardenGroveCOSTARAnnieNoebelCornersCommJul142026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 132000000,
+ "amount_label": "$132.0M",
+ "size_label": "402 units",
+ "units": 402,
+ "sqft": null,
+ "address": "12091 Bayport St",
+ "occupancy_pct": null,
+ "year_built": 1968,
+ "summary": "Eagle Partners and The Vistria Group have purchased Crystal View Apartments, a 402-unit apartment mixed-income workforce housing community located in Garden Grove, for $132. 5 mil, or $329."
+ },
+ {
+ "id": "34204",
+ "title": "Oceanside Res Community Trades for $77.5 Mil",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34204",
+ "image": "https://www.rentv.com/images/dynamic/news_20262_3851SherbourneDrOceansideDANKIRKSEYClaudiaLipmannCBREJul142026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 77500000,
+ "amount_label": "$77.5M",
+ "size_label": "240 units · 14.24 acres",
+ "units": 240,
+ "sqft": null,
+ "address": "3851 Sherbourne Dr",
+ "occupancy_pct": null,
+ "year_built": 1975,
+ "summary": "Sunterra, a 240-unit multifamily community in Oceanside, has traded hands in a $77. 5 mil ($323k/unit) transaction."
+ },
+ {
+ "id": "34210",
+ "title": "BKM Capital Partners Boosts Pac Northwest Holdings with Purchase of 400k sf Industrial Buy in Washington",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34210",
+ "image": "https://www.rentv.com/images/dynamic/news_20270_SouthcenterWestBusinessParkTukwilaWASuleAygorenConnectJul142026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Industrial",
+ "city": "Tukwila",
+ "state": "WA",
+ "amount": 4000000,
+ "amount_label": "$4.0M",
+ "size_label": "21 units · 19 acres",
+ "units": 21,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": 84,
+ "year_built": null,
+ "summary": "BKM Capital Partners has acquired a five-building Metro Seattle industrial portfolio. The assets, located in the markets premier South King County industrial corridor, were acquired on behalf of BKM Industrial Value Fund III."
+ },
+ {
+ "id": "34203",
+ "title": "The Bascom Group Pays Over $53 Mil for OC Res Community",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34203",
+ "image": "https://www.rentv.com/images/dynamic/news_20261_6968CampusDrBuenaParkJerryFinkBascomJul132026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 53000000,
+ "amount_label": "$53.0M",
+ "size_label": "183 units · 8.7 acres",
+ "units": 183,
+ "sqft": null,
+ "address": "6968 Campus Dr",
+ "occupancy_pct": null,
+ "year_built": 1985,
+ "summary": "The Bascom Group LLC has acquired Castlewood Park Apartments, a low-density, 183-unit value-add apartment community in Buena Park, for $53. 125 mil, or $290k per unit."
+ },
+ {
+ "id": "34199",
+ "title": "Leasing Assignment for 650k sf San Francisco Office Tower Awarded to CBRE",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34199",
+ "image": "https://www.rentv.com/images/dynamic/news_20259_415NatomaSFranciscoIrineSpivakCBREJun162026.jpg",
+ "txn_type": "Lease",
+ "property_type": "Office",
+ "city": "San Francisco",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "The Meridian Group and Fenway Capital Advisors have awarded the leasing assignment for 415 Natoma, a 25-story, 650k sf Class A office tower in San Franciscos SoMa neighborhood, to CBRE. Delivered in 2022 as the trophy office component of the 5M mixed-use development, 415 Natoma is the newest high-rise office tower in"
+ },
+ {
+ "id": "34202",
+ "title": "SummerHill Apartment Communities Breaks Ground on New 251-Unit Bay Area Res Development",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34202",
+ "image": "https://www.rentv.com/images/dynamic/news_20260_11ElcaminoRealSMateoAnneMonaghanJul72026.jpg",
+ "txn_type": "Development",
+ "property_type": "Multifamily",
+ "city": "Bay Area",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": "251 units · 2.2 acres",
+ "units": 251,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "SummerHill Apartment Communities has broken ground on 11 El Camino Real, a new 251-unit multifamily rental community in San Mateo County. The development, which includes 38 essential affordable homes at various income levels, brings a contemporary, pedestrian-oriented design to a prominent 2."
+ },
+ {
+ "id": "34196",
+ "title": "Rubicon Point Partners Spends $53 Mil on Silicon Valley Medical/Office Property",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34196",
+ "image": "",
+ "txn_type": "Sale",
+ "property_type": "Office",
+ "city": "Silicon Valley",
+ "state": null,
+ "amount": 53000000,
+ "amount_label": "$53.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": 93,
+ "year_built": null,
+ "summary": "In a recent Bay Area office trade, Rubicon Point Partners purchased Wolfe Square, a 118k sf Class A asset in Cupertino, for $53 mil ($449/sf). The two-building, multi-tenant office/medical property is located at 10050 and 10080 N."
+ },
+ {
+ "id": "34195",
+ "title": "Borstein Enterprises Picks Up 282k sf OC Industrial Asset",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34195",
+ "image": "https://www.rentv.com/images/dynamic/news_20256_15339BarrancaPkwyIrvineSavannahDurbinCWJul62026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Industrial",
+ "city": "Phoenix",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": "16.2 acres",
+ "units": null,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "Borstein Enterprises has acquired Barranca Business Park, a 13-building industrial business park totaling 282k sf of space on 16. 2 acres of land in Irvine."
+ },
+ {
+ "id": "34188",
+ "title": "Chino Distribution Facility Leased by Auto Parts Distributor in $42 Mil Deal",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34188",
+ "image": "https://www.rentv.com/images/dynamic/news_20252_16380EuclidAveChinoElisabethManvilleSmartJul22026.jpg",
+ "txn_type": "Lease",
+ "property_type": "Industrial",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 42000000,
+ "amount_label": "$42.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": "16380 Euclid Ave",
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "An auto parts distribution company, believed to be Titanium Plus Auto Parts, has inked a lease for a 522. 3k sf distribution facility in Chino for $42 mil."
+ },
+ {
+ "id": "34186",
+ "title": "Santa Clara Industrial Property Trades for $25.75 Mil",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34186",
+ "image": "https://www.rentv.com/images/dynamic/news_20250_651MartinAveSClaraGraceLewisJLLJul12026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Industrial",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 25750000,
+ "amount_label": "$25.8M",
+ "size_label": "7.17 acres",
+ "units": null,
+ "sqft": null,
+ "address": "651 Martin Ave",
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "In a Bay Area development purchase, Lift Partners acquired a 7. 17-acre infill site in Santa Clara for $25."
+ },
+ {
+ "id": "34181",
+ "title": "Redwood West and Panattoni Buy The Pike Destination Center in Long Beach",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34181",
+ "image": "https://www.rentv.com/images/dynamic/news_20245_95SPineAveLongBeachJessicaSpauldingJun302026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Retail",
+ "city": "Long Beach",
+ "state": null,
+ "amount": 20000000,
+ "amount_label": "$20.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": "95 S. Pine Ave",
+ "occupancy_pct": 91,
+ "year_built": 2003,
+ "summary": "Redwood West, in partnership with Panattoni, has acquired The Pike, a 394. 6k sf waterfront retail, dining, and entertainment lifestyle center in Long Beach."
+ },
+ {
+ "id": "34180",
+ "title": "Kennedy Wilson and Jamison Form Partnership to Develop 4,000 Units of Affordable Housing in Los Angeles",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34180",
+ "image": "https://www.rentv.com/images/dynamic/news_20244_350SHopeBruceBeckJune302026.jpg",
+ "txn_type": "Development",
+ "property_type": "Office",
+ "city": "Los Angeles",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": "4,000 units",
+ "units": 4000,
+ "sqft": null,
+ "address": "350 S. Figueroa St",
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "Kennedy Wilson and L. A."
+ },
+ {
+ "id": "34086",
+ "title": "Oak Park Investments and Arise Investments Acquire Westlake Village Warehouse/Office Building in $19 Mil Transaction",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34086",
+ "image": "https://www.rentv.com/images/dynamic/news_20161_5785LinderoCanyonRdWstlkVillageDannySoroudiAriseJune22026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Office",
+ "city": "Phoenix",
+ "state": null,
+ "amount": 19000000,
+ "amount_label": "$19.0M",
+ "size_label": null,
+ "units": null,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "Oak Park Investments and Arise Investments, in partnership with Nelnet Real Estate Ventures, have acquired a 98. 1k sf warehouse facility in Westlake Village for $19."
+ },
+ {
+ "id": "34177",
+ "title": "Holland Partner Group Closes $105 Mil Bay Area Res Buy",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34177",
+ "image": "https://www.rentv.com/images/dynamic/news_20242_1432WestSanCarlosSt.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Bay Area",
+ "state": null,
+ "amount": 105000000,
+ "amount_label": "$105.0M",
+ "size_label": "218 units",
+ "units": 218,
+ "sqft": null,
+ "address": "1432 West San Carlos St",
+ "occupancy_pct": null,
+ "year_built": 2015,
+ "summary": "In a significant NoCal res trade, Holland Partner Group has purchased Meridian at Midtown, a 218-unit luxury apartment complex in San Jose. The asset was sold by Essex Property Trust in a deal valued at around $105 mil ($482k/unit)."
+ },
+ {
+ "id": "34172",
+ "title": "Haven Housing buys 264-Unit Res Community in Austin Metro Area",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34172",
+ "image": "https://www.rentv.com/images/dynamic/news_20237_15001StrathavenPassPflugervilleTXElliottGolanConnectJun262026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Austin",
+ "state": null,
+ "amount": null,
+ "amount_label": null,
+ "size_label": "264 units",
+ "units": 264,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": 2018,
+ "summary": "Haven Housing has acquired Wells Park Apartments, a 264-unit garden-style apartment community in Pflugerville, TX, a growing suburb of Austin. Haven Housing is the affordable and multifamily housing platform of BH Properties."
+ },
+ {
+ "id": "34174",
+ "title": "MG Properties Acquires Two Multifamily Properties in Gresham, OR in Deals Totaling $60.8 Mil",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34174",
+ "image": "https://www.rentv.com/images/dynamic/news_20239_ParksideApartmentsGreshamORPaulKasebergMGPropJune262026.jpg",
+ "txn_type": "Sale",
+ "property_type": "Multifamily",
+ "city": "Gresham",
+ "state": "OR",
+ "amount": 60800000,
+ "amount_label": "$60.8M",
+ "size_label": "337 units · 32 acres",
+ "units": 337,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "San Diego-based MG Properties purchased two multifamily communities totaling 337 units in Gresham, OR in deals worth just over $60 mil. The acquisitions were of Mountain High Apartments for $18."
+ },
+ {
+ "id": "34171",
+ "title": "Leading AI Firm Leases 1.1 msf of Space at Luke Field in Glendale, AZ",
+ "url": "https://www.rentv.com/content/homepage/mainnews/news/34171",
+ "image": "https://www.rentv.com/images/dynamic/news_20236_7733LitchfieldRdGlendaleAZStaceyHershauerJun242026.jpg",
+ "txn_type": "Lease",
+ "property_type": "Industrial",
+ "city": "Glendale",
+ "state": "AZ",
+ "amount": 152000000,
+ "amount_label": "$152.0M",
+ "size_label": "140 acres",
+ "units": null,
+ "sqft": null,
+ "address": "13543 A and 13543 B Northern Ave",
+ "occupancy_pct": 100,
+ "year_built": null,
+ "summary": "There have been two full-building leases to report at Luke Field, a 2. 4 msf development from Lincoln Property Company in Glendale, AZ."
+ },
+ {
+ "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",
+ "image": "https://www.rentv.com/images/dynamic/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg",
+ "txn_type": "Financing",
+ "property_type": "Multifamily",
+ "city": "Seattle",
+ "state": null,
+ "amount": 82500000,
+ "amount_label": "$82.5M",
+ "size_label": "352 units · 16 acres",
+ "units": 352,
+ "sqft": null,
+ "address": null,
+ "occupancy_pct": null,
+ "year_built": null,
+ "summary": "A Harrison Street Asset Management and Security Properties joint venture has secured a refi loan of $82. 5 mil on Olin Fields, a 352-unit, garden-style apartment community in the Seattle suburb of Everett, WA."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/markets.json b/data/markets.json
index eaf70a45..79183fe0 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-23T03:45:03.643Z",
+ "fetched_at": "2026-07-23T04:05:04.724Z",
"source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
"curve": [
{
diff --git a/data/news.json b/data/news.json
index a28e970d..77ae501b 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
{
"source": "rentv.com — live",
- "fetched_at": "2026-07-23T03:45:03.162Z",
+ "fetched_at": "2026-07-23T04:05:04.370Z",
"count": 30,
"items": [
{
diff --git a/data/ticker.json b/data/ticker.json
index 94628011..0b98cd9a 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
{
- "fetched_at": "2026-07-23T03:45:03.616Z",
+ "fetched_at": "2026-07-23T04:05:04.697Z",
"la_office_vac": {
"label": "LA OFFICE VAC",
"value": "23.1%",
diff --git a/public/deals.html b/public/deals.html
new file mode 100644
index 00000000..b1369fac
--- /dev/null
+++ b/public/deals.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<title>CRE Deal Listings — RENTV.com</title>
+<meta name="description" content="Live commercial real estate deal listings — sales, leases, financings and developments across the Western U.S., parsed from RENTV.com transaction reporting.">
+<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>
+ :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(--wash);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 .live{display:inline-flex;align-items:center;gap:7px;letter-spacing:.05em;text-transform:uppercase;font-size:11px}
+ .util .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)}}
+ .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{background:#fff;border-bottom:2px solid var(--ink);padding:16px 0 12px}
+ .mast .wrap{display:flex;align-items:center;justify-content:space-between}
+ .logo{font-family:var(--serif);font-weight:700;font-size:32px;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)}
+ nav{background:#fff;border-bottom:1px solid var(--line)}
+ 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:13px 8px;border-bottom:3px solid transparent;white-space:nowrap}
+ nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
+ .head{padding:30px 0 6px}
+ .head h1{font-family:var(--serif);font-size:34px;letter-spacing:-.5px}
+ .head p{color:var(--sub);font-size:14px;margin-top:4px;max-width:820px}
+ .head .fresh{font-size:12px;color:var(--sub);margin-top:8px}
+ .controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:18px 0 6px}
+ .pill{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:7px 13px;border:1px solid var(--line);border-radius:999px;background:#fff;cursor:pointer;color:var(--sub)}
+ .pill.on{background:var(--ink);color:#fff;border-color:var(--ink)}
+ .controls input{margin-left:auto;font-family:var(--sans);font-size:13px;padding:8px 12px;border:1px solid var(--line);border-radius:6px;min-width:200px}
+ .count{font-size:12px;color:var(--sub);margin:6px 0 16px}
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
+ .card{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s,transform .15s}
+ .card:hover{box-shadow:0 8px 24px rgba(0,0,0,.09);transform:translateY(-2px)}
+ .card .ph{aspect-ratio:16/9;background:var(--wash) center/cover no-repeat;position:relative}
+ .card .badges{position:absolute;top:10px;left:10px;display:flex;gap:6px}
+ .badge{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:4px 8px;border-radius:4px;color:#fff;background:var(--ink)}
+ .badge.txn{background:var(--red)}
+ .card .amt{position:absolute;bottom:10px;right:10px;background:rgba(20,23,26,.92);color:#fff;font-family:var(--serif);font-weight:700;font-size:18px;padding:4px 10px;border-radius:5px}
+ .card .body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:8px;flex:1}
+ .card h3{font-family:var(--serif);font-size:17px;line-height:1.25}
+ .card .meta{font-size:12px;color:var(--sub);font-weight:600;display:flex;flex-wrap:wrap;gap:4px 10px}
+ .card .meta b{color:var(--ink)}
+ .card .sum{font-size:13px;color:var(--sub);line-height:1.5}
+ .card .more{margin-top:auto;font-size:12px;font-weight:700;color:var(--red);text-transform:uppercase;letter-spacing:.04em}
+ .empty{background:#fff;border:1px dashed var(--line);border-radius:10px;padding:40px;text-align:center;color:var(--sub)}
+ footer{background:var(--ink);color:#8b939b;margin-top:40px;padding:26px 0;font-size:12px}
+ footer .wrap{display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
+ @media(max-width:900px){.grid{grid-template-columns:1fr 1fr}}
+ @media(max-width:600px){.grid{grid-template-columns:1fr}.controls input{margin-left:0;width:100%}}
+</style>
+</head>
+<body>
+ <div class="util"><div class="wrap">
+ <span class="live"><span class="dot"></span> Live CRE deal flow</span>
+ <div class="acts"><a href="/subscribe">Newsletters</a><a href="/advertise">Advertise</a><a class="sub" href="/subscribe">Subscribe</a></div>
+ </div></div>
+ <header class="mast"><div class="wrap">
+ <a class="logo" href="/">REN<span>TV</span>.com<small>Commercial Real Estate News</small></a>
+ </div></header>
+ <nav><div class="wrap">
+ <a href="/">Home</a><a href="/deals" class="on">Deals</a><a href="/sales">Sales</a><a href="/leases">Leases</a><a href="/financing">Financing</a><a href="/development">Development</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker & Owner Desk</a><a href="/conferences">Conferences</a><a href="/blog">Blog</a>
+ </div></nav>
+ <main class="wrap">
+ <div class="head">
+ <h1>CRE Deal Listings</h1>
+ <p>Every commercial real estate transaction RENTV.com is reporting right now — sales, leases, financings and developments across the Western U.S. — parsed into structured listings with price, property type, size and location. Each card links to the full story.</p>
+ <div class="fresh" id="fresh">Loading…</div>
+ </div>
+ <div class="controls" id="txns"></div>
+ <div class="controls" id="types"></div>
+ <div class="controls"><input id="q" type="search" placeholder="Search city, property, party…" autocomplete="off"></div>
+ <div class="count" id="count"></div>
+ <div class="grid" id="grid"></div>
+ </main>
+ <footer><div class="wrap"><span>© RENTV.com — Commercial Real Estate News</span><span>Deal listings parsed from live RENTV.com reporting, refreshed every 20 min</span></div></footer>
+<script>
+const $=s=>document.querySelector(s);
+let ALL=[], fTxn='', fType='', fQ='';
+const TXNS=['Sale','Lease','Financing','Development'];
+const TYPES=['Multifamily','Office','Industrial','Retail','Hospitality','Mixed-Use','Land'];
+function esc(s){return (s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));}
+function pills(host,list,cur,pick){
+ host.innerHTML=`<button class="pill${cur===''?' on':''}" data-v="">All</button>`+
+ list.map(v=>`<button class="pill${cur===v?' on':''}" data-v="${v}">${v}</button>`).join('');
+ host.querySelectorAll('.pill').forEach(b=>b.onclick=()=>{pick(b.dataset.v);});
+}
+function render(){
+ const rows=ALL.filter(d=>
+ (!fTxn||d.txn_type===fTxn)&&(!fType||d.property_type===fType)&&
+ (!fQ||`${d.title} ${d.city} ${d.state} ${d.summary}`.toLowerCase().includes(fQ)));
+ $('#count').textContent=`${rows.length} listing${rows.length===1?'':'s'}${fTxn?' · '+fTxn:''}${fType?' · '+fType:''}`;
+ $('#grid').innerHTML=rows.length?rows.map(d=>{
+ const loc=[d.city,d.state].filter(Boolean).join(', ');
+ const ph=d.image?`background-image:url('${esc(d.image)}')`:'';
+ return `<a class="card" href="${esc(d.url)}" target="_blank" rel="noopener">
+ <div class="ph" style="${ph}">
+ <div class="badges"><span class="badge txn">${esc(d.txn_type)}</span><span class="badge">${esc(d.property_type)}</span></div>
+ ${d.amount_label?`<span class="amt">${esc(d.amount_label)}</span>`:''}
+ </div>
+ <div class="body">
+ <h3>${esc(d.title)}</h3>
+ <div class="meta">${loc?`<span>📍 <b>${esc(loc)}</b></span>`:''}${d.size_label?`<span>📐 ${esc(d.size_label)}</span>`:''}${d.year_built?`<span>🏗 ${d.year_built}</span>`:''}${d.occupancy_pct!=null?`<span>${d.occupancy_pct}% leased</span>`:''}</div>
+ ${d.summary?`<div class="sum">${esc(d.summary)}</div>`:''}
+ <div class="more">Read the full deal →</div>
+ </div></a>`;
+ }).join(''):'<div class="empty">No listings match these filters yet. Clear them or check back — the feed refreshes every 20 minutes.</div>';
+}
+(async()=>{
+ try{
+ const d=await (await fetch('/api/deals')).json();
+ ALL=d.deals||[];
+ $('#fresh').textContent=d.fetched_at?('Deal flow as of '+new Date(d.fetched_at).toLocaleString()+' · source: RENTV.com'):'';
+ }catch(e){ $('#fresh').textContent='Could not load deal listings — reload to retry.'; }
+ const pickTxn=v=>{fTxn=v;pills($('#txns'),TXNS,fTxn,pickTxn);render();};
+ const pickType=v=>{fType=v;pills($('#types'),TYPES,fType,pickType);render();};
+ pills($('#txns'),TXNS,fTxn,pickTxn);
+ pills($('#types'),TYPES,fType,pickType);
+ $('#q').oninput=e=>{fQ=e.target.value.trim().toLowerCase();render();};
+ render();
+})();
+</script>
+</body>
+</html>
diff --git a/public/index.html b/public/index.html
index b1a6521d..5cffc7a5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -96,7 +96,7 @@
<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="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker & 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>
+ <nav><a href="/deals">Deals</a><a href="/markets">Markets</a><a href="/la-commercial">LA Commercial</a><a href="/desk">Broker & 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 across the Western U.S. since 1998.</div>
diff --git a/scripts/pull-deals.mjs b/scripts/pull-deals.mjs
new file mode 100644
index 00000000..d72eced3
--- /dev/null
+++ b/scripts/pull-deals.mjs
@@ -0,0 +1,150 @@
+#!/usr/bin/env node
+// Deal-listings engine for rentv-v1: turns REAL rentv.com transaction stories into
+// structured deal cards (data/deals.json). $0 plain fetch, no bot-wall. Steve-approved
+// source (rentv.com is allowed, 2026-07-22). Fills the previously-empty listings surface.
+//
+// Each rentv.com "news" item is actually a CRE transaction (sale / lease / financing /
+// development). We fetch each story body and parse: $ amount, property type, location,
+// size, occupancy, address, date, and a clean summary. Fields we can't find stay null —
+// never fabricated (honest-empty rule).
+import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+const HERE = dirname(fileURLToPath(import.meta.url));
+const DATA = join(HERE, '..', 'data'); mkdirSync(DATA, { recursive: true });
+
+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');
+ return s;
+}
+const clean = (t) => t.replace(/&/g, '&').replace(/'/g, "'").replace(/"/g, '"')
+ .replace(/ | /g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+
+// $51.8 mil / $1.2 bil / $985,000 → normalized number of dollars
+function parseAmount(text) {
+ let m = text.match(/\$\s?([\d,]+(?:\.\d+)?)\s?(bil|billion|mil|million|k)?/i);
+ if (!m) return { amount: null, amount_label: null };
+ let n = parseFloat(m[1].replace(/,/g, ''));
+ const unit = (m[2] || '').toLowerCase();
+ if (/bil/.test(unit)) n *= 1e9; else if (/mil/.test(unit)) n *= 1e6; else if (unit === 'k') n *= 1e3;
+ const label = n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `$${(n / 1e6).toFixed(1)}M` : `$${n.toLocaleString()}`;
+ return { amount: Math.round(n), amount_label: label };
+}
+
+// Classify from the TITLE first (it states the deal precisely); only fall back to
+// the body when the title is inconclusive — bodies leak generic words ("loan",
+// "units") that mis-drive the type of an office/retail deal.
+function txnOf(t) {
+ return /refinanc|\brefi\b|obtains? (a )?(new )?loan|senior loan|recap|bridge loan/.test(t) ? 'Financing'
+ : /\bsold\b|sells|\bsale\b|acquir|buys|purchas|trades|closes on|pays|spends|snaps up|picks up|fetches|changes hands|nets \$|lands \$|works out to/.test(t) ? 'Sale'
+ : /leas|tenant|renew|signs? (a|new)|inks? a/.test(t) ? 'Lease'
+ : /break(s)? ground|develop|deliver|top(s|ped) out|complet|construction|to build|unveils?|plan(s|ned) to|underway|rises|proposes?/.test(t) ? 'Development'
+ : null;
+}
+function typeOf(t) {
+ return /office|\bhq\b|high-?rise|tower/.test(t) ? 'Office'
+ : /industrial|warehouse|logistics|distribution|\bflex\b/.test(t) ? 'Industrial'
+ : /retail|shopping|mall|grocery|storefront|strip (center|mall)/.test(t) ? 'Retail'
+ : /hotel|hospitality|resort|motel/.test(t) ? 'Hospitality'
+ : /medical|life science|\blab\b|biotech/.test(t) ? 'Medical/Life Science'
+ : /mixed-?use/.test(t) ? 'Mixed-Use'
+ : /self-?storage/.test(t) ? 'Self-Storage'
+ : /multifamily|apartment|\bres\b|residential communit/.test(t) ? 'Multifamily'
+ : /\bland\b|\bsite\b|\bacres?\b/.test(t) ? 'Land'
+ : null;
+}
+function classify(title, body) {
+ const tl = title.toLowerCase(), bl = body.toLowerCase();
+ const txn = txnOf(tl) || txnOf(bl) || 'Deal';
+ // for type, allow a bare "unit(s)" body signal only as a last resort
+ const type = typeOf(tl) || typeOf(bl) || (/\bunit(s)?\b/.test(bl) ? 'Multifamily' : 'Commercial');
+ return { txn, type };
+}
+
+// "Gilbert, AZ" / "Los Angeles, CA" — first City, ST in the title, else body.
+// Falls back to a bare city (title lead word or "in <City>") when no state is stated.
+const KNOWN_CITY = /\b(Bay Area|Silicon Valley|Inland Empire|Orange County|South Bay|East Bay|Downtown L\.?A\.?|Los Angeles|San Francisco|San Diego|Sacramento|Fresno|Oakland|San Jose|Long Beach|Irvine|Anaheim|Pasadena|Phoenix|Scottsdale|Tucson|Mesa|Las Vegas|Reno|Henderson|Seattle|Portland|Denver|Dallas|Houston|Austin|Miami|Atlanta|Chicago|New York|Boston)\b/;
+function parseLocation(title, body) {
+ // City token is the ONE word right before ", ST", optionally extended left by a
+ // known multi-word prefix (San/Los/Long/…) — this stops the match from swallowing
+ // preceding verbs/entities ("…Trust Buys Gilbert, AZ" → "Gilbert", not "Property Trust").
+ const re = /\b((?:San|Los|Long|Santa|New|Bay|Silicon|Inland|Orange|South|East|West|North|Downtown|Mission|Culver|Sherman)\s)?([A-Z][a-z.\-]{2,}),\s?([A-Z]{2})\b/;
+ // Title is authoritative for place — a body can name several cities (comps,
+ // buyer HQ) and pick the wrong one. Only fall to the body if the title has none.
+ const join = (m) => ((m[1] || '') + m[2]).trim().replace(/[’']s$/, '');
+ const tm = title.match(re);
+ if (tm) return { city: join(tm), state: tm[3] };
+ const tk = title.match(KNOWN_CITY);
+ if (tk) return { city: tk[1], state: null };
+ const bm = body.match(re);
+ if (bm) return { city: join(bm), state: bm[3] };
+ const bk = body.match(KNOWN_CITY);
+ if (bk) return { city: bk[1], state: null };
+ return { city: null, state: null };
+}
+
+function parseSize(text) {
+ const units = text.match(/([\d,]+)[- ]unit/i);
+ const sf = text.match(/([\d,]+)\s?(?:sf|sq\.?\s?ft|square[ -]feet|square[ -]foot)/i);
+ const acres = text.match(/([\d,.]+)[- ]acre/i);
+ const parts = [];
+ if (units) parts.push(`${units[1]} units`);
+ if (sf) parts.push(`${sf[1]} SF`);
+ if (acres) parts.push(`${acres[1]} acres`);
+ return { size_label: parts.join(' · ') || null,
+ units: units ? +units[1].replace(/,/g, '') : null,
+ sqft: sf ? +sf[1].replace(/,/g, '') : null };
+}
+
+function parseAddress(body) {
+ const m = body.match(/(?:located at|situated at|address is|at)\s+(\d{2,6}\s[A-Z][A-Za-z0-9.\- ]+?(?:Rd|Road|St|Street|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Way|Ln|Lane|Pl|Place|Ct|Court|Pkwy|Parkway|Hwy|Highway)\.?)/);
+ return m ? m[1].trim() : null;
+}
+function parseOccupancy(body) { const m = body.match(/(\d{1,3})%\s?(?:leased|occup)/i); return m ? +m[1] : null; }
+function parseYearBuilt(body) { const m = body.match(/built in (\d{4})/i); return m ? +m[1] : null; }
+
+// two-sentence summary from the body (drop the leading title echo + date stamp)
+function summarize(title, body) {
+ let b = body;
+ const ti = b.indexOf(title);
+ if (ti >= 0) b = b.slice(ti + title.length);
+ b = b.replace(/^\s*\d{1,2}\/\d{1,2}\/\d{2,4}\s*/, '').trim();
+ const sentences = b.match(/[^.!?]+[.!?]+/g) || [b];
+ return sentences.slice(0, 2).join(' ').trim().slice(0, 320);
+}
+
+try {
+ const news = JSON.parse(readFileSync(join(DATA, 'news.json'), 'utf8'));
+ const items = news.items || [];
+ const deals = [];
+ for (const it of items) {
+ let body = '';
+ try { body = clean(await fetchDecoded(it.url)); } catch { /* skip body, keep headline */ }
+ const hay = it.title + ' ' + body;
+ const { amount, amount_label } = parseAmount(it.title.match(/\$/) ? it.title : hay);
+ const { txn, type } = classify(it.title, body);
+ const loc = parseLocation(it.title, body);
+ const size = parseSize(hay);
+ deals.push({
+ id: it.id, title: it.title, url: it.url, image: it.image || '',
+ txn_type: txn, property_type: type,
+ city: loc.city, state: loc.state,
+ amount, amount_label,
+ size_label: size.size_label, units: size.units, sqft: size.sqft,
+ address: parseAddress(body),
+ occupancy_pct: parseOccupancy(body),
+ year_built: parseYearBuilt(body),
+ summary: summarize(it.title, body) || null,
+ });
+ }
+ const payload = { source: 'rentv.com — live deal listings', fetched_at: new Date().toISOString(), count: deals.length, deals };
+ writeFileSync(join(DATA, 'deals.json'), JSON.stringify(payload, null, 1));
+ const withAmt = deals.filter((d) => d.amount).length;
+ console.log(`OK: parsed ${deals.length} deal listings (${withAmt} with $ amount)`);
+} catch (e) {
+ console.error('deals pull failed:', e.message, '— last-good deals.json kept');
+ process.exitCode = 1;
+}
diff --git a/server.js b/server.js
index 1507d81f..db9359d3 100644
--- a/server.js
+++ b/server.js
@@ -65,6 +65,24 @@ app.get('/api/la-commercial', (_q, r) => {
r.json(readJSON('la-commercial.json', { marquee: [], recent: [], firms: [] }));
});
+// ── Deals: structured CRE transaction listings parsed from LIVE rentv.com stories
+// (refreshed by scripts/pull-deals.mjs cron). The real-listings surface. Supports
+// ?type=, ?txn=, ?q= filters + ?limit so the UI can slice by property/deal type. ──
+app.get('/api/deals', (req, r) => {
+ r.set('Cache-Control', 'public, max-age=300');
+ const data = readJSON('deals.json', { deals: [] });
+ let deals = Array.isArray(data.deals) ? data.deals : [];
+ const { type, txn, q } = req.query;
+ if (type) deals = deals.filter((d) => (d.property_type || '').toLowerCase() === String(type).toLowerCase());
+ if (txn) deals = deals.filter((d) => (d.txn_type || '').toLowerCase() === String(txn).toLowerCase());
+ if (q) {
+ const needle = String(q).toLowerCase();
+ deals = deals.filter((d) => `${d.title} ${d.city} ${d.state} ${d.summary}`.toLowerCase().includes(needle));
+ }
+ const limit = Math.min(parseInt(req.query.limit, 10) || deals.length, 200);
+ r.json({ source: data.source, fetched_at: data.fetched_at, count: deals.length, deals: deals.slice(0, limit) });
+});
+
// ── Newsletter capture: append-only local JSONL (NO external send-to-list). ──
const SUBS = path.join(DATA, 'subscribers.jsonl');
const MAX_SUBS = 50000; // hard ceiling — guards against disk-fill / append abuse
@@ -212,6 +230,7 @@ app.get('/api/article/:id', async (req, res) => {
// live feed, one consistent model site-wide; no thin static section pages). ──
const SECTIONS = ['financing', 'leases', 'development', 'retail', 'multifamily', 'industrial', 'sales'];
SECTIONS.forEach(s => app.get('/' + s, (_q, r) => r.redirect(301, '/?cat=' + s.charAt(0).toUpperCase() + s.slice(1))));
+app.get('/deals', (_q, r) => r.sendFile(path.join(PUB, 'deals.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')));
← 4a206b7a auto-save: 2026-07-22T20:46:37 (3 files) — data/markets.json
·
back to Rentv 2026
·
chore: refactor (drop dead decode branch) + v0.6.0 (deals-li 16ef70d0 →