← 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
M viewer/index.htmlM viewer/server.mjs
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 →