← back to Trending Dw
trending: self-host all card images locally (scripts/localize-images.js)
00c19c00eade78649d6a2730458e6d4c1e2bdf41 · 2026-07-06 21:56:57 -0700 · Steve Abrams
'show all images, download locally first' — download the 3 distinct remote wallpapersback.com
by-id images into public/assets/wpb/ and rewrite bestsellers.json to serve them same-origin.
No more hotlinking (even our own WPB) → faster + resilient if the source host is down.
Wired into refresh.sh --scout so future refreshes self-host too. Idempotent, $0 (fetches
our own public images). 22 imaged cards now 100% local; 27 gap cards stay on the pending
affordance (no our-own design yet — awaits gated generation).
Files touched
M data/bestsellers.jsonA public/assets/wpb/138.pngA public/assets/wpb/57669.pngA public/assets/wpb/57678.pngA scripts/localize-images.jsM scripts/refresh.sh
Diff
commit 00c19c00eade78649d6a2730458e6d4c1e2bdf41
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 6 21:56:57 2026 -0700
trending: self-host all card images locally (scripts/localize-images.js)
'show all images, download locally first' — download the 3 distinct remote wallpapersback.com
by-id images into public/assets/wpb/ and rewrite bestsellers.json to serve them same-origin.
No more hotlinking (even our own WPB) → faster + resilient if the source host is down.
Wired into refresh.sh --scout so future refreshes self-host too. Idempotent, $0 (fetches
our own public images). 22 imaged cards now 100% local; 27 gap cards stay on the pending
affordance (no our-own design yet — awaits gated generation).
---
data/bestsellers.json | 27 ++++++++++---------
public/assets/wpb/138.png | Bin 0 -> 88910 bytes
public/assets/wpb/57669.png | Bin 0 -> 43903 bytes
public/assets/wpb/57678.png | Bin 0 -> 65229 bytes
scripts/localize-images.js | 63 ++++++++++++++++++++++++++++++++++++++++++++
scripts/refresh.sh | 1 +
6 files changed, 78 insertions(+), 13 deletions(-)
diff --git a/data/bestsellers.json b/data/bestsellers.json
index 9f70e63..531bb3e 100644
--- a/data/bestsellers.json
+++ b/data/bestsellers.json
@@ -221,7 +221,7 @@
"signal": "Most-installed heritage wallpaper",
"signalRank": 98,
"url": "https://williammorriswallpaper.co/blogs/guides/wallpaper-trends-2026",
- "image": "https://wallpapersback.com/designs/img/by-id/138",
+ "image": "/assets/wpb/138.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Apricot Garden No.138",
"isOurOriginal": true
@@ -240,7 +240,7 @@
"signal": "Running ahead of forecast",
"signalRank": 92,
"url": "https://williammorriswallpaper.co/blogs/guides/wallpaper-trends-2026",
- "image": "https://wallpapersback.com/designs/img/by-id/138",
+ "image": "/assets/wpb/138.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Apricot Garden No.138",
"isOurOriginal": true
@@ -259,7 +259,7 @@
"signal": "Standout heritage/damask 2026",
"signalRank": 89,
"url": "https://www.worldofwallpaper.com/us/blog/Wallpaper-Trends-2026-Colours-Textures-Patterns",
- "image": "https://wallpapersback.com/designs/img/by-id/138",
+ "image": "/assets/wpb/138.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Apricot Garden No.138",
"isOurOriginal": true
@@ -297,7 +297,7 @@
"signal": "Romantic maximalism leader",
"signalRank": 84,
"url": "https://www.countryandtownhouse.com/interiors/wallpaper-trends-2026/",
- "image": "https://wallpapersback.com/designs/img/by-id/57678",
+ "image": "/assets/wpb/57678.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Terracotta Atelier No.57678",
"isOurOriginal": true
@@ -316,7 +316,7 @@
"signal": "Chinoiserie crane/blossom comeback",
"signalRank": 86,
"url": "https://vivawalls.com/products/chinoiserie-garden-mural-wallpaper-vintage-botanical-scene",
- "image": "https://wallpapersback.com/designs/img/by-id/57669",
+ "image": "/assets/wpb/57669.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Chamois Reverie No.57669",
"isOurOriginal": true
@@ -335,7 +335,7 @@
"signal": "Crane chinoiserie statement wall",
"signalRank": 81,
"url": "https://www.miltonandking.co.uk/product/oriental-garden-mural/",
- "image": "https://wallpapersback.com/designs/img/by-id/57669",
+ "image": "/assets/wpb/57669.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Chamois Reverie No.57669",
"isOurOriginal": true
@@ -354,7 +354,7 @@
"signal": "\"Our most popular\" saffron chinoiserie",
"signalRank": 78,
"url": "https://www.feathr.com/global/wall-murals/yellow-floral-chinoiserie-wall-mural-fe48605/",
- "image": "https://wallpapersback.com/designs/img/by-id/57669",
+ "image": "/assets/wpb/57669.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Chamois Reverie No.57669",
"isOurOriginal": true
@@ -373,7 +373,7 @@
"signal": "Mural searches +1,150% — botanical panorama #1",
"signalRank": 88,
"url": "https://www.giffywalls.com/blogs/inspiration-hub/mural-wallpaper-the-biggest-wall-trend-of-2026",
- "image": "https://wallpapersback.com/designs/img/by-id/57669",
+ "image": "/assets/wpb/57669.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Chamois Reverie No.57669",
"isOurOriginal": true
@@ -392,7 +392,7 @@
"signal": "5,511 reviews — Star Seller",
"signalRank": 80,
"url": "https://www.etsy.com/featured/etsy-wallpaper",
- "image": "https://wallpapersback.com/designs/img/by-id/57669",
+ "image": "/assets/wpb/57669.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Chamois Reverie No.57669",
"isOurOriginal": true
@@ -487,7 +487,7 @@
"signal": "Top-seller cottagecore florals",
"signalRank": 72,
"url": "https://www.spoonflower.com/en/inspiration/collections",
- "image": "https://wallpapersback.com/designs/img/by-id/138",
+ "image": "/assets/wpb/138.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Apricot Garden No.138",
"isOurOriginal": true
@@ -525,7 +525,7 @@
"signal": "Bestselling minimalist floral",
"signalRank": 71,
"url": "https://livetteswallpaper.com/blogs/interior-blog/meet-our-bestselling-wallpapers",
- "image": "https://wallpapersback.com/designs/img/by-id/138",
+ "image": "/assets/wpb/138.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Apricot Garden No.138",
"isOurOriginal": true
@@ -841,7 +841,7 @@
"signal": "Designer vintage-floral pick 2026",
"signalRank": 79,
"url": "https://www.goodhousekeeping.com/home/decorating-ideas/g70161758/wallpaper-trends-2026/",
- "image": "https://wallpapersback.com/designs/img/by-id/138",
+ "image": "/assets/wpb/138.png",
"imageSource": "Wallpaper's Back (our original)",
"imageDesign": "Apricot Garden No.138",
"isOurOriginal": true
@@ -879,5 +879,6 @@
"genBrief": "Original Retro Geometric in Rust — generated in our own hand from live trend research, settlement-gated. Trend signal: Primal Geometry trend (rank 68)."
}
],
- "imagesAttachedAt": "2026-07-06"
+ "imagesAttachedAt": "2026-07-06",
+ "imagesLocalizedAt": "2026-07-06"
}
\ No newline at end of file
diff --git a/public/assets/wpb/138.png b/public/assets/wpb/138.png
new file mode 100644
index 0000000..100aa78
Binary files /dev/null and b/public/assets/wpb/138.png differ
diff --git a/public/assets/wpb/57669.png b/public/assets/wpb/57669.png
new file mode 100644
index 0000000..d0cc445
Binary files /dev/null and b/public/assets/wpb/57669.png differ
diff --git a/public/assets/wpb/57678.png b/public/assets/wpb/57678.png
new file mode 100644
index 0000000..f8fab8a
Binary files /dev/null and b/public/assets/wpb/57678.png differ
diff --git a/scripts/localize-images.js b/scripts/localize-images.js
new file mode 100644
index 0000000..679afab
--- /dev/null
+++ b/scripts/localize-images.js
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+// localize-images.js — download any REMOTE card images into public/assets/wpb/ and rewrite
+// bestsellers.json to serve them SAME-ORIGIN. So the trending site never hotlinks (even our
+// own wallpapersback.com) — faster, and resilient if the source host is down.
+// $0 (just fetches our own public images). Idempotent: skips ids already downloaded.
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+
+const ROOT = path.join(__dirname, '..');
+const BEST = path.join(ROOT, 'data', 'bestsellers.json');
+const DEST = path.join(ROOT, 'public', 'assets', 'wpb');
+fs.mkdirSync(DEST, { recursive: true });
+
+const EXT = { 'image/png':'png', 'image/jpeg':'jpg', 'image/webp':'webp', 'image/gif':'gif', 'image/avif':'avif' };
+
+function fetchBuf(url){
+ return new Promise((resolve, reject) => {
+ https.get(url, res => {
+ if (res.statusCode !== 200) { res.resume(); return reject(new Error(url + ' → HTTP ' + res.statusCode)); }
+ const type = (res.headers['content-type'] || '').split(';')[0].trim();
+ const chunks = []; res.on('data', c => chunks.push(c));
+ res.on('end', () => resolve({ buf: Buffer.concat(chunks), type }));
+ }).on('error', reject);
+ });
+}
+
+(async () => {
+ const data = JSON.parse(fs.readFileSync(BEST, 'utf8'));
+ // collect distinct remote images (skip already-local /assets paths)
+ const remotes = new Map(); // url -> [items]
+ for (const it of data.items){
+ if (it.image && /^https?:\/\//.test(it.image)) (remotes.get(it.image) || remotes.set(it.image, []).get(it.image)).push(it);
+ }
+ if (!remotes.size) { console.log('no remote images — already fully local.'); return; }
+
+ let ok = 0, fail = 0;
+ for (const [url, items] of remotes){
+ // derive a stable local name: wpb/<by-id-id>.<ext>, else a hash of the url
+ const m = url.match(/\/by-id\/(\d+)/);
+ const stem = m ? m[1] : 'img-' + Buffer.from(url).toString('base64url').slice(0, 12);
+ try {
+ // find an already-downloaded file for this stem (idempotent)
+ const existing = fs.readdirSync(DEST).find(f => f.startsWith(stem + '.'));
+ let fname;
+ if (existing) { fname = existing; }
+ else {
+ const { buf, type } = await fetchBuf(url);
+ if (!buf.length) throw new Error('empty body');
+ fname = stem + '.' + (EXT[type] || 'png');
+ fs.writeFileSync(path.join(DEST, fname), buf);
+ }
+ const localPath = '/assets/wpb/' + fname;
+ for (const it of items) it.image = localPath;
+ ok++; console.log(` ${url} → ${localPath} (${items.length} card${items.length>1?'s':''})`);
+ } catch (e) {
+ fail++; console.log(` SKIP ${url} — ${e.message} (left as remote)`);
+ }
+ }
+ data.imagesLocalizedAt = new Date().toLocaleDateString('en-CA');
+ fs.writeFileSync(BEST, JSON.stringify(data, null, 2));
+ console.log(`localized ${ok} remote image${ok!==1?'s':''}${fail?`, ${fail} skipped`:''}. All served same-origin from /assets/wpb/.`);
+})();
diff --git a/scripts/refresh.sh b/scripts/refresh.sh
index ce2496c..eeeafb0 100755
--- a/scripts/refresh.sh
+++ b/scripts/refresh.sh
@@ -24,6 +24,7 @@ case "${1:-}" in
FEED="${2:?usage: refresh.sh --scout <scout-feed.json>}"
node scripts/scout-to-bestsellers.js "$FEED" # $0 deterministic mapper → data/bestsellers.json (today's date)
node scripts/attach-images.js # re-attach our-own images (mapper rewrites the file) + GAP flags
+ node scripts/localize-images.js # download any remote card images → self-host from /assets/wpb/
reload
echo "LIVE refresh done from $FEED — $(date)"
;;
← c9753c2 trending: pin staleness to Steve's TZ (America/Los_Angeles)
·
back to Trending Dw
·
chore: lint, refactor, v0.1.1 (session close) eec6dc5 →