[object Object]

← back to Rentv V1

Localize all rentv.com photos + drop link-outs (serve everything locally)

f934418ae00b09a86f9287422666664363ef5010 · 2026-07-23 14:07:33 -0700 · Steve Abrams

- Shared scripts/lib/localize.cjs downloads rentv.com property photos into
  public/img/news/ and returns local /img/news/ paths (fail-closed to '').
- Wired into pull-news.mjs, pull-deals.mjs (durable across cron) and
  server.js fetchArticle (live article hero). Backfilled news.json + deals.json.
- article.html: removed the two clickable 'View on RENTV.com' link-outs;
  attribution kept as plain text.
- .gitignore the downloaded photo cache (regenerable), keep dir via .gitkeep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f934418ae00b09a86f9287422666664363ef5010
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 23 14:07:33 2026 -0700

    Localize all rentv.com photos + drop link-outs (serve everything locally)
    
    - Shared scripts/lib/localize.cjs downloads rentv.com property photos into
      public/img/news/ and returns local /img/news/ paths (fail-closed to '').
    - Wired into pull-news.mjs, pull-deals.mjs (durable across cron) and
      server.js fetchArticle (live article hero). Backfilled news.json + deals.json.
    - article.html: removed the two clickable 'View on RENTV.com' link-outs;
      attribution kept as plain text.
    - .gitignore the downloaded photo cache (regenerable), keep dir via .gitkeep.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .gitignore               |  4 ++++
 data/deals.json          | 56 +++++++++++++++++++++++-----------------------
 data/markets.json        |  2 +-
 data/news.json           | 58 ++++++++++++++++++++++++------------------------
 data/ticker.json         |  2 +-
 public/article.html      |  8 +++----
 public/img/news/.gitkeep |  0
 scripts/lib/localize.cjs | 53 +++++++++++++++++++++++++++++++++++++++++++
 scripts/pull-deals.mjs   |  6 +++++
 scripts/pull-news.mjs    |  7 ++++++
 server.js                |  3 +++
 11 files changed, 135 insertions(+), 64 deletions(-)

diff --git a/.gitignore b/.gitignore
index 961ccec..1b5d619 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,7 @@ node_modules/
 .DS_Store
 data/subscribers.jsonl
 data/posts.json
+
+# Localized rentv.com photos — regenerable cache, served locally (not source)
+public/img/news/*
+!public/img/news/.gitkeep
diff --git a/data/deals.json b/data/deals.json
index 5847be9..82b2f51 100644
--- a/data/deals.json
+++ b/data/deals.json
@@ -7,7 +7,7 @@
    "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",
+   "image": "/img/news/news_20296_4353EElliotRdGilbertAZDarcieGiacchettoJul212026.jpg",
    "txn_type": "Financing",
    "property_type": "Multifamily",
    "city": "Gilbert",
@@ -26,7 +26,7 @@
    "id": "34236",
    "title": "The Swig Company Obtains Refi on Office High-Rise in San Francisco’s Financial District",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34236",
-   "image": "https://www.rentv.com/images/dynamic/news_20294_220MontgomeryStSFNancyAmaralA2N2Jul162026.jpg",
+   "image": "/img/news/news_20294_220MontgomeryStSFNancyAmaralA2N2Jul162026.jpg",
    "txn_type": "Financing",
    "property_type": "Office",
    "city": "San Francisco",
@@ -45,7 +45,7 @@
    "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",
+   "image": "/img/news/news_20291_6633ValleyHiDrSacramentoDavidHulmeTailwindJul182026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Sacramento",
@@ -64,7 +64,7 @@
    "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",
+   "image": "/img/news/news_20297_585WestSierraAveFresnoDaveEbelingJul212026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Fresno",
@@ -83,7 +83,7 @@
    "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",
+   "image": "/img/news/news_20293_MosiacLongBeachAnneMonaghanJul202026.jpg",
    "txn_type": "Sale",
    "property_type": "Retail",
    "city": "Long Beach",
@@ -102,7 +102,7 @@
    "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",
+   "image": "/img/news/news_20283_550AllertonRedwoodCityNickTrombolaNWMKJul162026.jpg",
    "txn_type": "Sale",
    "property_type": "Office",
    "city": "Bay Area",
@@ -121,7 +121,7 @@
    "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",
+   "image": "/img/news/news_20285_860SandhillAveCarsonElisabethManvilleSmartJul162026.jpg",
    "txn_type": "Sale",
    "property_type": "Industrial",
    "city": "Phoenix",
@@ -140,7 +140,7 @@
    "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",
+   "image": "/img/news/news_20278_3150ERayRdGilbertKimHoidalCBREJul132026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Gilbert",
@@ -178,7 +178,7 @@
    "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",
+   "image": "/img/news/news_20286_TupeloAlleyPortlandPaulKaseburgMGPropJul162026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Portland",
@@ -197,7 +197,7 @@
    "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",
+   "image": "/img/news/news_20277_21300WOxnardStWHillsHollyJenvyJul152026.jpg",
    "txn_type": "Development",
    "property_type": "Multifamily",
    "city": "Phoenix",
@@ -216,7 +216,7 @@
    "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",
+   "image": "/img/news/news_20274_1402LakeTappsPkwySEAuburnWABruceBeckJul152026.jpg",
    "txn_type": "Sale",
    "property_type": "Retail",
    "city": "Auburn",
@@ -235,7 +235,7 @@
    "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",
+   "image": "/img/news/news_20267_12091BayportStGardenGroveCOSTARAnnieNoebelCornersCommJul142026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Phoenix",
@@ -254,7 +254,7 @@
    "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",
+   "image": "/img/news/news_20262_3851SherbourneDrOceansideDANKIRKSEYClaudiaLipmannCBREJul142026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Phoenix",
@@ -273,7 +273,7 @@
    "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",
+   "image": "/img/news/news_20270_SouthcenterWestBusinessParkTukwilaWASuleAygorenConnectJul142026.jpg",
    "txn_type": "Sale",
    "property_type": "Industrial",
    "city": "Tukwila",
@@ -292,7 +292,7 @@
    "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",
+   "image": "/img/news/news_20261_6968CampusDrBuenaParkJerryFinkBascomJul132026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Phoenix",
@@ -311,7 +311,7 @@
    "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",
+   "image": "/img/news/news_20259_415NatomaSFranciscoIrineSpivakCBREJun162026.jpg",
    "txn_type": "Lease",
    "property_type": "Office",
    "city": "San Francisco",
@@ -330,7 +330,7 @@
    "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",
+   "image": "/img/news/news_20260_11ElcaminoRealSMateoAnneMonaghanJul72026.jpg",
    "txn_type": "Development",
    "property_type": "Multifamily",
    "city": "Bay Area",
@@ -368,7 +368,7 @@
    "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",
+   "image": "/img/news/news_20256_15339BarrancaPkwyIrvineSavannahDurbinCWJul62026.jpg",
    "txn_type": "Sale",
    "property_type": "Industrial",
    "city": "Phoenix",
@@ -387,7 +387,7 @@
    "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",
+   "image": "/img/news/news_20252_16380EuclidAveChinoElisabethManvilleSmartJul22026.jpg",
    "txn_type": "Lease",
    "property_type": "Industrial",
    "city": "Phoenix",
@@ -406,7 +406,7 @@
    "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",
+   "image": "/img/news/news_20250_651MartinAveSClaraGraceLewisJLLJul12026.jpg",
    "txn_type": "Sale",
    "property_type": "Industrial",
    "city": "Phoenix",
@@ -425,7 +425,7 @@
    "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",
+   "image": "/img/news/news_20245_95SPineAveLongBeachJessicaSpauldingJun302026.jpg",
    "txn_type": "Sale",
    "property_type": "Retail",
    "city": "Long Beach",
@@ -444,7 +444,7 @@
    "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",
+   "image": "/img/news/news_20244_350SHopeBruceBeckJune302026.jpg",
    "txn_type": "Development",
    "property_type": "Office",
    "city": "Los Angeles",
@@ -463,7 +463,7 @@
    "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",
+   "image": "/img/news/news_20161_5785LinderoCanyonRdWstlkVillageDannySoroudiAriseJune22026.jpg",
    "txn_type": "Sale",
    "property_type": "Office",
    "city": "Phoenix",
@@ -482,7 +482,7 @@
    "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",
+   "image": "/img/news/news_20242_1432WestSanCarlosSt.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Bay Area",
@@ -501,7 +501,7 @@
    "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",
+   "image": "/img/news/news_20237_15001StrathavenPassPflugervilleTXElliottGolanConnectJun262026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Austin",
@@ -520,7 +520,7 @@
    "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",
+   "image": "/img/news/news_20239_ParksideApartmentsGreshamORPaulKasebergMGPropJune262026.jpg",
    "txn_type": "Sale",
    "property_type": "Multifamily",
    "city": "Gresham",
@@ -539,7 +539,7 @@
    "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",
+   "image": "/img/news/news_20236_7733LitchfieldRdGlendaleAZStaceyHershauerJun242026.jpg",
    "txn_type": "Lease",
    "property_type": "Industrial",
    "city": "Glendale",
@@ -558,7 +558,7 @@
    "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",
+   "image": "/img/news/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg",
    "txn_type": "Financing",
    "property_type": "Multifamily",
    "city": "Seattle",
diff --git a/data/markets.json b/data/markets.json
index 85a6671..f95b0ed 100644
--- a/data/markets.json
+++ b/data/markets.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T20:45:57.860Z",
+  "fetched_at": "2026-07-23T21:06:23.228Z",
   "source": "Yahoo Finance — live (CBOE Treasury yields, Dow Jones/MSCI REIT indices)",
   "curve": [
     {
diff --git a/data/news.json b/data/news.json
index 0433c35..5688775 100644
--- a/data/news.json
+++ b/data/news.json
@@ -1,6 +1,6 @@
 {
  "source": "rentv.com — live",
- "fetched_at": "2026-07-23T20:45:57.532Z",
+ "fetched_at": "2026-07-23T21:06:22.958Z",
  "count": 30,
  "items": [
   {
@@ -8,56 +8,56 @@
    "title": "Gilbert, AZ Multifamily Community Refinanced with New Loan for $51.8 Mil",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34238",
    "cat": "Financing",
-   "image": "https://www.rentv.com/images/dynamic/news_20296_4353EElliotRdGilbertAZDarcieGiacchettoJul212026.jpg"
+   "image": "/img/news/news_20296_4353EElliotRdGilbertAZDarcieGiacchettoJul212026.jpg"
   },
   {
    "id": "34236",
    "title": "The Swig Company Obtains Refi on Office High-Rise in San Francisco’s Financial District",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34236",
    "cat": "Financing",
-   "image": "https://www.rentv.com/images/dynamic/news_20294_220MontgomeryStSFNancyAmaralA2N2Jul162026.jpg"
+   "image": "/img/news/news_20294_220MontgomeryStSFNancyAmaralA2N2Jul162026.jpg"
   },
   {
    "id": "34233",
    "title": "Tailwind Investment Group Spends $65 Mil on Two Sacramento Res Properties",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34233",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20291_6633ValleyHiDrSacramentoDavidHulmeTailwindJul182026.jpg"
+   "image": "/img/news/news_20291_6633ValleyHiDrSacramentoDavidHulmeTailwindJul182026.jpg"
   },
   {
    "id": "34239",
    "title": "Fresno Multifamily Sale Works Out to $177k per Unit",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34239",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20297_585WestSierraAveFresnoDaveEbelingJul212026.jpg"
+   "image": "/img/news/news_20297_585WestSierraAveFresnoDaveEbelingJul212026.jpg"
   },
   {
    "id": "34235",
    "title": "Retail Portfolio in Downtown Long Beach Fetches $30 Mil",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34235",
    "cat": "Retail",
-   "image": "https://www.rentv.com/images/dynamic/news_20293_MosiacLongBeachAnneMonaghanJul202026.jpg"
+   "image": "/img/news/news_20293_MosiacLongBeachAnneMonaghanJul202026.jpg"
   },
   {
    "id": "34224",
    "title": "Shorenstein Buys 77k sf Bay Area Office Building",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34224",
    "cat": "Sales",
-   "image": "https://www.rentv.com/images/dynamic/news_20283_550AllertonRedwoodCityNickTrombolaNWMKJul162026.jpg"
+   "image": "/img/news/news_20283_550AllertonRedwoodCityNickTrombolaNWMKJul162026.jpg"
   },
   {
    "id": "34226",
    "title": "Carson Distribution Facility Acquired by WareSpace in a $23 Mil Deal",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34226",
    "cat": "Sales",
-   "image": "https://www.rentv.com/images/dynamic/news_20285_860SandhillAveCarsonElisabethManvilleSmartJul162026.jpg"
+   "image": "/img/news/news_20285_860SandhillAveCarsonElisabethManvilleSmartJul162026.jpg"
   },
   {
    "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",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20278_3150ERayRdGilbertKimHoidalCBREJul132026.jpg"
+   "image": "/img/news/news_20278_3150ERayRdGilbertKimHoidalCBREJul132026.jpg"
   },
   {
    "id": "34225",
@@ -71,63 +71,63 @@
    "title": "MG Properties Buys 188-Unit Portland Res Asset",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34227",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20286_TupeloAlleyPortlandPaulKaseburgMGPropJul162026.jpg"
+   "image": "/img/news/news_20286_TupeloAlleyPortlandPaulKaseburgMGPropJul162026.jpg"
   },
   {
    "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",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20277_21300WOxnardStWHillsHollyJenvyJul152026.jpg"
+   "image": "/img/news/news_20277_21300WOxnardStWHillsHollyJenvyJul152026.jpg"
   },
   {
    "id": "34214",
    "title": "Intercontinental Real Estate Corporation Closes $69 Mil Retail Buy in Washington",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34214",
    "cat": "Retail",
-   "image": "https://www.rentv.com/images/dynamic/news_20274_1402LakeTappsPkwySEAuburnWABruceBeckJul152026.jpg"
+   "image": "/img/news/news_20274_1402LakeTappsPkwySEAuburnWABruceBeckJul152026.jpg"
   },
   {
    "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",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20267_12091BayportStGardenGroveCOSTARAnnieNoebelCornersCommJul142026.jpg"
+   "image": "/img/news/news_20267_12091BayportStGardenGroveCOSTARAnnieNoebelCornersCommJul142026.jpg"
   },
   {
    "id": "34204",
    "title": "Oceanside Res Community Trades for $77.5 Mil",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34204",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20262_3851SherbourneDrOceansideDANKIRKSEYClaudiaLipmannCBREJul142026.jpg"
+   "image": "/img/news/news_20262_3851SherbourneDrOceansideDANKIRKSEYClaudiaLipmannCBREJul142026.jpg"
   },
   {
    "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",
    "cat": "Industrial",
-   "image": "https://www.rentv.com/images/dynamic/news_20270_SouthcenterWestBusinessParkTukwilaWASuleAygorenConnectJul142026.jpg"
+   "image": "/img/news/news_20270_SouthcenterWestBusinessParkTukwilaWASuleAygorenConnectJul142026.jpg"
   },
   {
    "id": "34203",
    "title": "The Bascom Group Pays Over $53 Mil for OC Res Community",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34203",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20261_6968CampusDrBuenaParkJerryFinkBascomJul132026.jpg"
+   "image": "/img/news/news_20261_6968CampusDrBuenaParkJerryFinkBascomJul132026.jpg"
   },
   {
    "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",
    "cat": "Sales",
-   "image": "https://www.rentv.com/images/dynamic/news_20259_415NatomaSFranciscoIrineSpivakCBREJun162026.jpg"
+   "image": "/img/news/news_20259_415NatomaSFranciscoIrineSpivakCBREJun162026.jpg"
   },
   {
    "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",
    "cat": "Development",
-   "image": "https://www.rentv.com/images/dynamic/news_20260_11ElcaminoRealSMateoAnneMonaghanJul72026.jpg"
+   "image": "/img/news/news_20260_11ElcaminoRealSMateoAnneMonaghanJul72026.jpg"
   },
   {
    "id": "34196",
@@ -141,77 +141,77 @@
    "title": "Borstein Enterprises Picks Up 282k sf OC Industrial Asset",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34195",
    "cat": "Industrial",
-   "image": "https://www.rentv.com/images/dynamic/news_20256_15339BarrancaPkwyIrvineSavannahDurbinCWJul62026.jpg"
+   "image": "/img/news/news_20256_15339BarrancaPkwyIrvineSavannahDurbinCWJul62026.jpg"
   },
   {
    "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",
    "cat": "Leases",
-   "image": "https://www.rentv.com/images/dynamic/news_20252_16380EuclidAveChinoElisabethManvilleSmartJul22026.jpg"
+   "image": "/img/news/news_20252_16380EuclidAveChinoElisabethManvilleSmartJul22026.jpg"
   },
   {
    "id": "34186",
    "title": "Santa Clara Industrial Property Trades for $25.75 Mil",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34186",
    "cat": "Industrial",
-   "image": "https://www.rentv.com/images/dynamic/news_20250_651MartinAveSClaraGraceLewisJLLJul12026.jpg"
+   "image": "/img/news/news_20250_651MartinAveSClaraGraceLewisJLLJul12026.jpg"
   },
   {
    "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",
    "cat": "Retail",
-   "image": "https://www.rentv.com/images/dynamic/news_20245_95SPineAveLongBeachJessicaSpauldingJun302026.jpg"
+   "image": "/img/news/news_20245_95SPineAveLongBeachJessicaSpauldingJun302026.jpg"
   },
   {
    "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",
    "cat": "Development",
-   "image": "https://www.rentv.com/images/dynamic/news_20244_350SHopeBruceBeckJune302026.jpg"
+   "image": "/img/news/news_20244_350SHopeBruceBeckJune302026.jpg"
   },
   {
    "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",
    "cat": "Industrial",
-   "image": "https://www.rentv.com/images/dynamic/news_20161_5785LinderoCanyonRdWstlkVillageDannySoroudiAriseJune22026.jpg"
+   "image": "/img/news/news_20161_5785LinderoCanyonRdWstlkVillageDannySoroudiAriseJune22026.jpg"
   },
   {
    "id": "34177",
    "title": "Holland Partner Group Closes $105 Mil Bay Area Res Buy",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34177",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20242_1432WestSanCarlosSt.jpg"
+   "image": "/img/news/news_20242_1432WestSanCarlosSt.jpg"
   },
   {
    "id": "34172",
    "title": "Haven Housing buys 264-Unit Res Community in Austin Metro Area",
    "url": "https://www.rentv.com/content/homepage/mainnews/news/34172",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20237_15001StrathavenPassPflugervilleTXElliottGolanConnectJun262026.jpg"
+   "image": "/img/news/news_20237_15001StrathavenPassPflugervilleTXElliottGolanConnectJun262026.jpg"
   },
   {
    "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",
    "cat": "Multifamily",
-   "image": "https://www.rentv.com/images/dynamic/news_20239_ParksideApartmentsGreshamORPaulKasebergMGPropJune262026.jpg"
+   "image": "/img/news/news_20239_ParksideApartmentsGreshamORPaulKasebergMGPropJune262026.jpg"
   },
   {
    "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",
    "cat": "Leases",
-   "image": "https://www.rentv.com/images/dynamic/news_20236_7733LitchfieldRdGlendaleAZStaceyHershauerJun242026.jpg"
+   "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": "https://www.rentv.com/images/dynamic/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg"
+   "image": "/img/news/news_20233_OlinFieldsEverettWABruceBeckJun242026.jpg"
   }
  ]
 }
\ No newline at end of file
diff --git a/data/ticker.json b/data/ticker.json
index 1d422ed..71a4f97 100644
--- a/data/ticker.json
+++ b/data/ticker.json
@@ -1,5 +1,5 @@
 {
-  "fetched_at": "2026-07-23T20:45:57.833Z",
+  "fetched_at": "2026-07-23T21:06:23.213Z",
   "la_office_vac": {
     "label": "LA OFFICE VAC",
     "value": "23.1%",
diff --git a/public/article.html b/public/article.html
index 7223914..e48d29a 100644
--- a/public/article.html
+++ b/public/article.html
@@ -92,7 +92,6 @@
 document.getElementById('today').textContent = new Date().toLocaleDateString(undefined,{weekday:'long',year:'numeric',month:'long',day:'numeric'});
 const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 const id=(location.pathname.split('/').filter(Boolean).pop()||'').replace(/[^0-9]/g,'');
-const RENTV='https://www.rentv.com/content/homepage/mainnews/news/'+id;
 // Generic titles the reader proxy returns when rentv.com doesn't server-render the story.
 const GENERIC=/^RENTV\.com\b|commercial real estate news and property/i;
 async function load(){
@@ -108,17 +107,16 @@ async function load(){
   // Prefer the feed's real headline; fall back to the proxy title only if it isn't the generic site title.
   const title = item.title || (d.title && !GENERIC.test(d.title) ? d.title : 'RENTV Story');
   const cat = item.cat || 'RENTV';
-  const src = item.url || d.url || RENTV;
   const hasBody = Array.isArray(d.body) && d.body.length>0;
   if(!item.title && !hasBody && art0.status!=='fulfilled'){
-    art.innerHTML='<p class="err">This story couldn’t be loaded right now. <a href="'+esc(RENTV)+'" target="_blank" rel="noopener">Read it on RENTV.com ↗</a><br><br><a href="/">← Back to the homepage</a></p>';
+    art.innerHTML='<p class="err">This story couldn’t be loaded right now.<br><br><a href="/">← Back to the homepage</a></p>';
     return;
   }
   document.title=title+' — RENTV.com';
   const hero=(d.image && !GENERIC.test(d.image))?'<div class="hero"><img src="'+esc(d.image)+'" alt="" loading="lazy" onerror="this.parentNode.style.background=\'linear-gradient(135deg,#1f2937,#c8102e)\';this.remove()"></div>':'<div class="hero"></div>';
   const body=hasBody
     ? d.body.map(p=>'<p>'+esc(p)+'</p>').join('')
-    : '<p>This story is published in full on RENTV.com. Read the complete report — including deal terms, parties and broker details — at the source link below.</p>';
+    : '<p>The full report — including deal terms, parties and broker details — is being republished here. Check back shortly for the complete story.</p>';
   art.innerHTML=
     '<div class="crumb"><a href="/">Home</a> › '+(['sales','leases','financing','development','multifamily','industrial','retail'].indexOf(cat.toLowerCase())>=0?'<a href="/'+esc(cat.toLowerCase())+'">'+esc(cat)+'</a>':'<span>'+esc(cat)+'</span>')+' › <span>Story</span></div>'+
     '<span class="chip">'+esc(cat)+' · Western U.S.</span>'+
@@ -126,7 +124,7 @@ async function load(){
     '<div class="byl">By RENTV Staff · live from rentv.com'+(d.fetched_at?(' · fetched '+new Date(d.fetched_at).toLocaleTimeString()):'')+'</div>'+
     hero+
     '<div class="body">'+body+'</div>'+
-    '<div class="src">📰 Republished live from RENTV.com. <a href="'+esc(src)+'" target="_blank" rel="noopener">View the full story on RENTV.com ↗</a></div>'+
+    '<div class="src">📰 Republished live from RENTV.com · Western U.S. commercial real estate news.</div>'+
     '<a class="back" href="/">← Back to all CRE news</a>';
 }
 const hamBtn=document.getElementById('hamBtn');
diff --git a/public/img/news/.gitkeep b/public/img/news/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/scripts/lib/localize.cjs b/scripts/lib/localize.cjs
new file mode 100644
index 0000000..16dafe7
--- /dev/null
+++ b/scripts/lib/localize.cjs
@@ -0,0 +1,53 @@
+'use strict';
+// Shared image localizer — the single source of truth for "never hotlink rentv.com".
+// Downloads a remote (rentv.com) property photo into public/img/news/ and returns the
+// LOCAL path (/img/news/<file>) the site serves itself. Used by BOTH the cron
+// (scripts/pull-news.mjs, via createRequire) and the live article proxy (server.js),
+// so a single fix covers every path that could otherwise leak an rentv.com asset URL.
+//
+// Contract:
+//   - already-local ("/img/..." or "") → returned unchanged (idempotent, safe to re-run).
+//   - download OK  → returns "/img/news/<sanitized-filename>" (served by express.static).
+//   - download FAIL → returns "" so the caller renders its gradient placeholder,
+//                     NEVER a leftover rentv.com URL (fail closed, not open).
+const fs = require('fs');
+const path = require('path');
+
+const IMG_DIR = path.join(__dirname, '..', '..', 'public', 'img', 'news');
+
+// Derive a safe, stable local filename from the remote URL's basename.
+function localName(url) {
+  const base = url.split('?')[0].split('#')[0].split('/').pop() || '';
+  const safe = base.replace(/[^A-Za-z0-9._-]/g, '_').slice(-160);
+  return safe || ('img_' + Buffer.from(url).toString('hex').slice(0, 16) + '.jpg');
+}
+
+async function localizeImage(url) {
+  if (!url || typeof url !== 'string') return '';
+  if (url.startsWith('/')) return url;                 // already local — leave it
+  if (!/^https?:\/\//i.test(url)) return '';           // not a fetchable absolute URL
+
+  const file = localName(url);
+  const dest = path.join(IMG_DIR, file);
+  const localPath = '/img/news/' + file;
+
+  // Cache hit: already downloaded and non-empty → reuse, no re-fetch.
+  try { if (fs.statSync(dest).size > 0) return localPath; } catch { /* not cached yet */ }
+
+  try {
+    fs.mkdirSync(IMG_DIR, { recursive: true });
+    const r = await fetch(url, {
+      headers: { 'User-Agent': 'Mozilla/5.0 RENTV-localizer' },
+      signal: AbortSignal.timeout(15000),
+    });
+    if (!r.ok) return '';
+    const buf = Buffer.from(await r.arrayBuffer());
+    if (!buf.length) return '';
+    fs.writeFileSync(dest, buf);
+    return localPath;
+  } catch {
+    return '';                                         // network/timeout → placeholder, not a leak
+  }
+}
+
+module.exports = { localizeImage, IMG_DIR };
diff --git a/scripts/pull-deals.mjs b/scripts/pull-deals.mjs
index d72eced..f2270b9 100644
--- a/scripts/pull-deals.mjs
+++ b/scripts/pull-deals.mjs
@@ -10,8 +10,12 @@
 import { writeFileSync, readFileSync, mkdirSync } from 'node:fs';
 import { dirname, join } from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { createRequire } from 'node:module';
 const HERE = dirname(fileURLToPath(import.meta.url));
 const DATA = join(HERE, '..', 'data'); mkdirSync(DATA, { recursive: true });
+// Localize deal-card photos into public/img/news/ — the Deals grid serves them locally,
+// never hotlinking rentv.com. Same shared localizer as the news pipeline.
+const { localizeImage } = createRequire(import.meta.url)('./lib/localize.cjs');
 
 async function fetchDecoded(url) {
   const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 RENTV-v1' }, signal: AbortSignal.timeout(20000) });
@@ -140,6 +144,8 @@ try {
       summary: summarize(it.title, body) || null,
     });
   }
+  // Download each remote photo locally and rewrite image → /img/news/<file> (fail → '' placeholder).
+  for (const d of deals) { d.image = await localizeImage(d.image); }
   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;
diff --git a/scripts/pull-news.mjs b/scripts/pull-news.mjs
index 4a15bf2..615fe23 100644
--- a/scripts/pull-news.mjs
+++ b/scripts/pull-news.mjs
@@ -4,8 +4,12 @@
 import { writeFileSync, mkdirSync } from 'node:fs';
 import { dirname, join } from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { createRequire } from 'node:module';
 const HERE = dirname(fileURLToPath(import.meta.url));
 const OUT = join(HERE, '..', 'data'); mkdirSync(OUT, { recursive: true });
+// Localize every rentv.com property photo into public/img/news/ so the SITE serves it —
+// the browser never hotlinks rentv.com. Shared with server.js's article proxy.
+const { localizeImage } = createRequire(import.meta.url)('./lib/localize.cjs');
 
 async function fetchDecoded(url) {
   const r = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 RENTV-v1' }, signal: AbortSignal.timeout(20000) });
@@ -38,6 +42,9 @@ try {
       : 'Sales';
     items.push({ id, title, url: m[1].startsWith('http') ? m[1] : 'https://www.rentv.com/' + m[1].replace(/^\//,''), cat, image: imgMap[id] || '' });
   }
+  // Download each remote photo locally and rewrite image → /img/news/<file>. On any
+  // download failure the field becomes '' (gradient placeholder) — never a leaked rentv.com URL.
+  for (const it of items) { it.image = await localizeImage(it.image); }
   const payload = { source: 'rentv.com — live', fetched_at: new Date().toISOString(), count: items.length, items };
   writeFileSync(join(OUT, 'news.json'), JSON.stringify(payload, null, 1));
   console.log('OK: pulled ' + items.length + ' live headlines');
diff --git a/server.js b/server.js
index 99ce2dc..a23ecd1 100644
--- a/server.js
+++ b/server.js
@@ -4,6 +4,7 @@
 // reader proxy (/api/article/:id) so stories are read INSIDE our modern shell.
 const fs = require('fs');
 const path = require('path');
+const { localizeImage } = require('./scripts/lib/localize.cjs'); // never hotlink rentv.com photos
 const express = require('express');
 const app = express();
 app.use(express.json({ limit: '16kb' }));
@@ -197,6 +198,8 @@ async function fetchArticle(id) {
   const imM = html.match(/["'](\/?(?:https?:\/\/[^"']*)?\/?images\/dynamic\/news_\d+_[^"']+\.(?:jpe?g|png))["']/i);
   let image = imM ? imM[1] : '';
   if (image && !/^https?:/i.test(image)) image = 'https://www.rentv.com/' + image.replace(/^\//, '');
+  // Copy the photo locally and serve it ourselves — the reader's browser never fetches rentv.com.
+  image = await localizeImage(image);
 
   // Body: take the region from the headline to a trailing marker, split on <br><br> into real paragraphs.
   const start = hM ? hM.index + hM[0].length : 0;

← 2149153 index: honor ?cat= so each hamburger section shows its own f  ·  back to Rentv V1  ·  auth: add Daniel viewer login b41691e →