[object Object]

← 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

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 →