[object Object]

← back to Dw Fleet Registry

viewer: proxy cross-origin hero images through /img — fleet microsites send CORP same-origin (helmet default) which blocked direct <img> hotlinking, leaving ~30 self-hosted hero-bg.jpg cards blank. Now 118/118 render

0e3363a9716d37451caf244277c0e148bb618d7f · 2026-06-01 14:07:22 -0700 · SteveStudio2

Files touched

Diff

commit 0e3363a9716d37451caf244277c0e148bb618d7f
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon Jun 1 14:07:22 2026 -0700

    viewer: proxy cross-origin hero images through /img — fleet microsites send CORP same-origin (helmet default) which blocked direct <img> hotlinking, leaving ~30 self-hosted hero-bg.jpg cards blank. Now 118/118 render
---
 viewer/index.html |  7 +++++--
 viewer/server.mjs | 19 +++++++++++++++++++
 2 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/viewer/index.html b/viewer/index.html
index b8609c8..ea9cca0 100644
--- a/viewer/index.html
+++ b/viewer/index.html
@@ -64,6 +64,9 @@
 
 <script>
 const $ = s => document.querySelector(s);
+// Route cross-origin hero images through the same-origin proxy (fleet sites send
+// CORP same-origin, which blocks direct <img> hotlinking). Local /static & /img pass through.
+const heroSrc = u => !u ? '' : (u.charAt(0)==='/' ? u : '/img?u='+encodeURIComponent(u));
 let SITES = [];
 const hueOf = hex => { if(!/^#?[0-9a-f]{6}$/i.test(hex||''))return 999; const n=parseInt(hex.replace('#',''),16); const r=(n>>16&255)/255,g=(n>>8&255)/255,b=(n&255)/255; const mx=Math.max(r,g,b),mn=Math.min(r,g,b),d=mx-mn; let h=0; if(d){ if(mx===r)h=((g-b)/d)%6; else if(mx===g)h=(b-r)/d+2; else h=(r-g)/d+4; } return Math.round(h*60+360)%360; };
 const accentOf = s => (s.local&&s.local.port?null:null) || (s.manifest&&s.manifest.theme&&s.manifest.theme.accent) || (s.local&&s.local.theme&&s.local.theme.accent) || '#9c7a4d';
@@ -99,7 +102,7 @@ function render(){
     a.innerHTML =
       '<div class="accentbar" style="background:'+s._accent+'"></div>'+
       '<div class="thumb" data-domain="'+s.domain+'" style="background-color:'+s._accent+'22">'+
-        (s.heroImage?'<img class="heroimg" loading="lazy" src="'+s.heroImage.replace(/"/g,'%22')+'" alt="" onerror="this.remove()">':'')+
+        (s.heroImage?'<img class="heroimg" loading="lazy" src="'+heroSrc(s.heroImage)+'" alt="" onerror="this.remove()">':'')+
         '<div class="ph">'+(s.siteName||s.domain).slice(0,18)+'</div></div>'+
       '<div class="body">'+
         '<div class="dom">'+s.domain+'</div>'+
@@ -123,7 +126,7 @@ function lazyHeroes(){
       if(el.querySelector('img.heroimg')) continue;   // baked hero already painted
       const d=el.dataset.domain;
       fetch('/api/hero?domain='+encodeURIComponent(d)).then(r=>r.json()).then(j=>{
-        if(j.image){ const img=new Image(); img.onload=()=>{ el.style.backgroundImage='url('+j.image+')'; const ph=el.querySelector('.ph'); if(ph)ph.style.display='none'; }; img.src=j.image; }
+        if(j.image){ const u=heroSrc(j.image); const img=new Image(); img.onload=()=>{ el.style.backgroundImage='url('+u+')'; const ph=el.querySelector('.ph'); if(ph)ph.style.display='none'; }; img.src=u; }
       }).catch(()=>{});
     }
   },{rootMargin:'300px'});
diff --git a/viewer/server.mjs b/viewer/server.mjs
index a1584ae..9bfcdb9 100644
--- a/viewer/server.mjs
+++ b/viewer/server.mjs
@@ -14,6 +14,7 @@ const DIR = path.dirname(new URL(import.meta.url).pathname);
 const REG = path.join(DIR, '..', 'dw-fleet-registry.json');
 const PORT = process.env.PORT || 9774;
 const heroCache = new Map();   // domain -> {image, at}
+const imgCache = new Map();    // url -> {ct, buf}  (proxy cache; many fleet images send CORP same-origin and can't be hotlinked cross-origin)
 const HERO_TTL = 10 * 60 * 1000;        // re-resolve after 10 min so fixed sites recover
 const JUNK = /(?:close-?icon|sprite|favicon|logo|badge|placeholder|loader|spinner|1x1|pixel|blank|transparent|data:image|\.svg(?:[?#]|$)|_(?:48|64|96|125|150|180)x)/i;
 
@@ -78,6 +79,24 @@ async function liveHero(domain) {
 const server = http.createServer(async (req, res) => {
   const u = new URL(req.url, 'http://localhost');
   if (u.pathname === '/') return send(res, 200, 'text/html; charset=utf-8', fs.readFileSync(path.join(DIR, 'index.html')));
+  if (u.pathname === '/img') {
+    const target = u.searchParams.get('u') || '';
+    if (!/^https?:\/\//i.test(target)) return send(res, 400, 'text/plain', 'bad url');
+    const hit = imgCache.get(target);
+    if (hit) { res.writeHead(200, { 'Content-Type': hit.ct, 'Cache-Control': 'public, max-age=86400' }); return res.end(hit.buf); }
+    try {
+      const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 12000);
+      const r = await fetch(target, { signal: ctrl.signal, redirect: 'follow', headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': target } });
+      clearTimeout(t);
+      if (!r.ok) return send(res, 502, 'text/plain', 'upstream ' + r.status);
+      const ct = r.headers.get('content-type') || 'image/jpeg';
+      const buf = Buffer.from(await r.arrayBuffer());
+      if (imgCache.size > 400) imgCache.clear();   // crude cap
+      imgCache.set(target, { ct, buf });
+      res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': 'public, max-age=86400' });
+      return res.end(buf);
+    } catch { return send(res, 502, 'text/plain', 'proxy error'); }
+  }
   if (u.pathname.startsWith('/static/')) {
     const safe = u.pathname.replace(/\.\.+/g, '').replace(/^\/static\//, '');
     const fp = path.join(DIR, 'static', safe);

← f5aaaa0 heroes = truly-high-res: 12 real Shopify room settings (nich  ·  back to Dw Fleet Registry  ·  disable-grid sweep: 44 hero-4grid overlays off so unique hig 6006e2f →