← back to Dw Fleet Registry
fleet viewer: proxy-side image resize via optional sharp (/img?w=) — caps non-Shopify heroes to webp; sharp is an optionalDependency w/ graceful fallback
f1cf3385a98996ae246d4d6aa39ee8ad3273fb74 · 2026-06-01 15:48:51 -0700 · SteveStudio2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A package.jsonM viewer/index.htmlM viewer/server.mjs
Diff
commit f1cf3385a98996ae246d4d6aa39ee8ad3273fb74
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Mon Jun 1 15:48:51 2026 -0700
fleet viewer: proxy-side image resize via optional sharp (/img?w=) — caps non-Shopify heroes to webp; sharp is an optionalDependency w/ graceful fallback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
package.json | 8 ++++++++
viewer/index.html | 4 ++--
viewer/server.mjs | 20 ++++++++++++++++----
3 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..749b4cb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "dw-fleet-registry-viewer",
+ "private": true,
+ "description": "DW fleet registry viewer — zero hard deps; sharp is optional for /img proxy-side image resizing.",
+ "optionalDependencies": {
+ "sharp": "^0.33.5"
+ }
+}
diff --git a/viewer/index.html b/viewer/index.html
index 5864ddd..bae179c 100644
--- a/viewer/index.html
+++ b/viewer/index.html
@@ -155,8 +155,8 @@ const heroSrc = u => !u ? '' : (u.charAt(0)==='/' ? u : '/img?u='+encodeURICompo
const thumbSrc = (u, w) => {
if(!u) return '';
if(u.charAt(0)==='/') return u; // local /static, /img — already small
- if(/cdn\.shopify\.com/i.test(u)){ const [base,qs]=u.split('?'); const ps=new URLSearchParams(qs||''); ps.set('width', String(w||400)); u=base+'?'+ps.toString(); }
- return '/img?u='+encodeURIComponent(u);
+ if(/cdn\.shopify\.com/i.test(u)){ const [base,qs]=u.split('?'); const ps=new URLSearchParams(qs||''); ps.set('width', String(w||400)); return '/img?u='+encodeURIComponent(base+'?'+ps.toString()); }
+ return '/img?u='+encodeURIComponent(u)+'&w='+(w||400); // non-Shopify → server-side resize
};
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; };
diff --git a/viewer/server.mjs b/viewer/server.mjs
index 6f76413..8cea4db 100644
--- a/viewer/server.mjs
+++ b/viewer/server.mjs
@@ -14,6 +14,9 @@ const DIR = path.dirname(new URL(import.meta.url).pathname);
const REG = path.join(DIR, '..', 'dw-fleet-registry.json');
const CATALOG = path.join(DIR, '..', 'catalog-hires.json');
const PORT = process.env.PORT || 9774;
+// sharp is OPTIONAL — if it's not installed the proxy just serves full-res (graceful fallback).
+let sharp = null;
+try { sharp = (await import('sharp')).default; } catch { /* no sharp → /img?w= serves full-res */ }
let rescrapeRunning = false; // single-flight guard for /api/rescrape-heroes
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)
@@ -132,17 +135,26 @@ const server = http.createServer(async (req, res) => {
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);
+ // Optional server-side downscale: /img?u=…&w=400 resizes non-resizable (non-Shopify)
+ // images to webp so a 3MB hero becomes ~40KB. Shopify URLs already arrive small via ?width=.
+ const w = Math.min(2000, Math.max(64, parseInt(u.searchParams.get('w') || '0', 10) || 0));
+ const key = target + '|' + w;
+ const hit = imgCache.get(key);
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());
+ let ct = r.headers.get('content-type') || 'image/jpeg';
+ let buf = Buffer.from(await r.arrayBuffer());
+ // Downscale to webp when a width is requested, sharp is available, and it's a raster image.
+ if (w && sharp && /image\/(jpe?g|png|webp|avif)/i.test(ct)) {
+ try { buf = await sharp(buf).rotate().resize({ width: w, withoutEnlargement: true }).webp({ quality: 80 }).toBuffer(); ct = 'image/webp'; }
+ catch { /* keep original buffer on any resize failure */ }
+ }
if (imgCache.size > 400) imgCache.clear(); // crude cap
- imgCache.set(target, { ct, buf });
+ imgCache.set(key, { 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'); }
← af278f4 dw-header: hide ANY pre-existing <header> + propagate to fle
·
back to Dw Fleet Registry
·
rooms: gen-hero-rooms.mjs (generate room-setting heroes via ab9e050 →