← 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
A deploy/nginx-advertisers.confM public/index.htmlM server.js
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 →