[object Object]

← back to Rentv Ad Engine

make ad-engine mountable under a path prefix (BASE_PATH -> <base> injection + relative client paths); add nginx /advertisers proxy snippet — verified locally, standalone unaffected

87ad1aed128c253e559715b40c28127a390ca1b6 · 2026-08-07 10:02:35 -0700 · Steve Abrams

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

Files touched

Diff

commit 87ad1aed128c253e559715b40c28127a390ca1b6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 10:02:35 2026 -0700

    make ad-engine mountable under a path prefix (BASE_PATH -> <base> injection + relative client paths); add nginx /advertisers proxy snippet — verified locally, standalone unaffected
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 deploy/nginx-advertisers.conf | 20 ++++++++++++++++++++
 public/index.html             | 24 ++++++++++++------------
 server.js                     |  7 +++++++
 3 files changed, 39 insertions(+), 12 deletions(-)

diff --git a/deploy/nginx-advertisers.conf b/deploy/nginx-advertisers.conf
new file mode 100644
index 0000000..6d8494a
--- /dev/null
+++ b/deploy/nginx-advertisers.conf
@@ -0,0 +1,20 @@
+# Mount the RENTV Ad Engine at rentv.agentabrams.com/advertisers
+# Add this INSIDE the existing rentv.agentabrams.com server{} block on Kamatera
+# (the vhost that currently proxies to the rentv app on :9704). No change to the
+# rentv Express app itself — this is a sibling proxy to the ad-engine on :9789.
+#
+# The ad-engine pm2 process must run with BASE_PATH=/advertisers so it injects
+# <base href="/advertisers/"> and its relative api/img paths resolve correctly.
+
+location = /advertisers { return 301 /advertisers/; }
+
+location /advertisers/ {
+    proxy_pass http://127.0.0.1:9789/;   # trailing slash strips the /advertisers/ prefix
+    proxy_set_header Host              $host;
+    proxy_set_header X-Real-IP         $remote_addr;
+    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
+    proxy_set_header X-Forwarded-Proto $scheme;
+    # The ad-engine enforces its own Basic Auth (admin:DW2024!) — it is an INTERNAL
+    # sales tool (advertiser contacts + draft emails), so keep it gated. If the rentv
+    # vhost already has Basic Auth at the server level, that layer applies too.
+}
diff --git a/public/index.html b/public/index.html
index f0bc12f..1b03569 100644
--- a/public/index.html
+++ b/public/index.html
@@ -118,17 +118,17 @@ const COLS={
 
 async function boot(){
   const [a,g,m,s,fr]=await Promise.all([
-    fetch("/api/advertisers").then(r=>r.json()),
-    fetch("/api/guests").then(r=>r.json()),
-    fetch("/api/manifest").then(r=>r.json()),
-    fetch("/api/stats").then(r=>r.json()),
-    fetch("/api/frames").then(r=>r.json()).catch(()=>({frames:[]}))
+    fetch("api/advertisers").then(r=>r.json()),
+    fetch("api/guests").then(r=>r.json()),
+    fetch("api/manifest").then(r=>r.json()),
+    fetch("api/stats").then(r=>r.json()),
+    fetch("api/frames").then(r=>r.json()).catch(()=>({frames:[]}))
   ]);
   DATA.advertisers=a.advertisers||[]; DATA.guests=g.guests||[]; DATA.videos=m.videos||[];
   (fr.frames||[]).forEach(f=>FRAMES[f.id]=f);
-  try{SYN=await fetch("/api/synopses").then(r=>r.json())}catch{SYN={}}
-  try{DATA.emails=(await fetch("/api/emails").then(r=>r.json())).emails||[]}catch{DATA.emails=[]}
-  try{RV=await fetch("/api/rentvreview").then(r=>r.json())}catch{RV={}}
+  try{SYN=await fetch("api/synopses").then(r=>r.json())}catch{SYN={}}
+  try{DATA.emails=(await fetch("api/emails").then(r=>r.json())).emails||[]}catch{DATA.emails=[]}
+  try{RV=await fetch("api/rentvreview").then(r=>r.json())}catch{RV={}}
   $("#stat").innerHTML=`<b>${s.advertisers}</b> advertisers · <b>${s.guests}</b> guests · <b>${s.videos_rich}</b>/${s.videos_total} videos transcribed`;
   $("#dens").value=LS("dens",4); document.documentElement.style.setProperty("--cols",$("#dens").value);
   document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("on",t.dataset.view===view));
@@ -229,7 +229,7 @@ function renderCards(main,L){
   }
   else L.forEach(v=>{
     const c=el("div","card"); const fr=FRAMES[v.id];
-    c.innerHTML=`${fr&&fr.poster?`<img src="${esc(fr.poster)}" loading="lazy" onclick="openEpisode(${v.id})" style="width:100%;border-radius:8px;cursor:pointer;border:1px solid var(--line)">`:""}
+    c.innerHTML=`${fr&&fr.poster?`<img src="${esc(fr.poster.replace(/^\//,''))}" loading="lazy" onclick="openEpisode(${v.id})" style="width:100%;border-radius:8px;cursor:pointer;border:1px solid var(--line)">`:""}
       <h3 onclick="openEpisode(${v.id})" style="font-size:14px">${esc(v.title)}</h3>
       <div class="kv">${v.air_date?"📅 "+esc(v.air_date)+" · ":""}⏱ ${(v.duration/60).toFixed(0)} min · 📝 ${(v.caption_chars/1000).toFixed(1)}k chars</div>
       ${fr&&fr.logos&&fr.logos.length?`<div class="row"><span class="kv">on-screen:</span>${fr.logos.map(l=>`<span class="chip">${esc(l)}</span>`).join("")}</div>`:""}
@@ -341,7 +341,7 @@ window.openEpisode=id=>{
     <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin:14px 0">
       <div><iframe src="https://player.vimeo.com/video/${id}" style="width:100%;aspect-ratio:16/9;border:0;border-radius:8px" allow="fullscreen" loading="lazy"></iframe>
         <div class="muted" style="margin-top:4px">Original video (if embedding is blocked, use “open on Vimeo”).</div></div>
-      <div>${fr.poster?`<img src="${esc(fr.poster)}" style="width:100%;border-radius:8px;border:1px solid var(--line)">`:""}
+      <div>${fr.poster?`<img src="${esc(fr.poster.replace(/^\//,''))}" style="width:100%;border-radius:8px;border:1px solid var(--line)">`:""}
         ${fr.logos&&fr.logos.length?`<div class="row" style="margin-top:6px"><span class="muted">on-screen:</span>${fr.logos.map(l=>`<span class="chip">${esc(l)}</span>`).join("")}</div>`:""}</div>
     </div>
     <h3>Synopsis</h3>
@@ -378,7 +378,7 @@ window.genEmail=(kind,name,epid)=>{
     const g=DATA.guests.find(x=>x.episode_id===epid&&x.name===name)||{};
     context=`Guest: ${g.name}\nTitle: ${g.title}\nCompany: ${g.company}\nFrom: ${g.location}\nTopics: ${(g.topics||[]).join(", ")}\nAppeared on: ${g.episode_title}`;
   }
-  fetch("/api/email",{method:"POST",headers:{"Content-Type":"application/json"},
+  fetch("api/email",{method:"POST",headers:{"Content-Type":"application/json"},
     body:JSON.stringify({kind,name,context})})
     .then(r=>r.json()).then(d=>{
       if(d.error){out.innerHTML=`<div class="mailpanel" style="color:#e5989b">${esc(d.error)}</div>`;return;}
@@ -398,7 +398,7 @@ window.regenDraft=(btn,name)=>{
   const card=btn.closest(".card"); const a=DATA.advertisers.find(x=>x.name===name)||{};
   const context=`Advertiser: ${a.name}\nCategory: ${a.category}\nWebsites: ${(a.websites||[]).join(", ")}\nPhones: ${(a.phones||[]).join(", ")}\nWhat they do: ${a.pitch}\nSponsored ${a.appearances} episode(s): ${(a.episodes||[]).map(e=>e.title).join("; ")}`;
   btn.textContent="…";
-  fetch("/api/email",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({kind:"advertiser",name,context})})
+  fetch("api/email",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({kind:"advertiser",name,context})})
     .then(r=>r.json()).then(d=>{if(d.subject)card.querySelector(".es").value=d.subject; if(d.body)card.querySelector(".eb").value=d.body; btn.textContent="↻ Regenerate";})
     .catch(()=>btn.textContent="↻ Regenerate");
 };
diff --git a/server.js b/server.js
index c5120d6..a4f6fbb 100644
--- a/server.js
+++ b/server.js
@@ -57,6 +57,13 @@ const server = http.createServer((req, res) => {
   if (!fp.startsWith(PUB) || !fs.existsSync(fp) || fs.statSync(fp).isDirectory()) {
     res.writeHead(404); return res.end("Not found");
   }
+  // base-path aware: when mounted under a prefix (e.g. rentv.agentabrams.com/advertisers),
+  // inject <base> so the page's relative api/img paths resolve under the prefix.
+  if (file === "index.html" && process.env.BASE_PATH) {
+    let h = fs.readFileSync(fp, "utf8").replace(/<head>/, `<head>\n<base href="${process.env.BASE_PATH.replace(/\/?$/, "/")}">`);
+    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
+    return res.end(h);
+  }
   res.writeHead(200, { "Content-Type": MIME[path.extname(fp)] || "application/octet-stream" });
   fs.createReadStream(fp).pipe(res);
 });

← c3eae5f Full REview reconciliation: producer-page harvest maps 20/22  ·  back to Rentv Ad Engine  ·  SoundCloud podcast source: harvest+transcribe rentvnews, ext 05adab8 →