[object Object]

← back to 4square Agentabrams

wire 19 more DW microsites — click → real products, not 'pending'

a00276f7975999f945a27afa1845746fde577a41 · 2026-05-14 09:28:56 -0700 · Steve

- sites.json: 20 wired sites now (was 1). Auto-probed every wallpaper/
  wallcovering domain in the rail for /api/products with valid JSON +
  image_url; 19 came back clean (corkwallcovering, silkwallpaper,
  linenwallpaper, chinoiseriewallpaper, metallicwallpaper, agedwallpaper,
  apartmentwallpaper, architecturalwallcoverings, blockprintedwallpaper,
  carmelwallpapers, fabricwallpaper, greenwallcoverings, museumwallpaper,
  mylarwallpaper, naturaltextilewallpaper, pastelwallpaper, saloonwallpaper,
  textilewallpaper, wallpapersback).

- server.js: new /api/site-products?site=<domain>&q=&limit= proxy.
  Validates site against an in-memory allowlist baked from sites.json at
  boot + on-file-change (fs.watchFile, 5s interval). TSD continues
  through its bespoke /api/catalog/search; everything else hits
  https://<site>/api/products with q/limit forwarded.

- index.html: fetches /api/site-products?site=… for every wired site.
  Normalizes shape (DW sites return either wrapped {items} or bare array;
  TSD returns {total, items}). Absolute https:// image URLs (Shopify CDN)
  used directly — only /api/art-archive paths go through the /img proxy.
  Items with no image are now skipped instead of rendering as black tiles.

Fixes Steve's complaint: clicking a site in the rail now loads real
products from that site, not 'Catalog connection pending'. 20× the
wired surface area in this revision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a00276f7975999f945a27afa1845746fde577a41
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu May 14 09:28:56 2026 -0700

    wire 19 more DW microsites — click → real products, not 'pending'
    
    - sites.json: 20 wired sites now (was 1). Auto-probed every wallpaper/
      wallcovering domain in the rail for /api/products with valid JSON +
      image_url; 19 came back clean (corkwallcovering, silkwallpaper,
      linenwallpaper, chinoiseriewallpaper, metallicwallpaper, agedwallpaper,
      apartmentwallpaper, architecturalwallcoverings, blockprintedwallpaper,
      carmelwallpapers, fabricwallpaper, greenwallcoverings, museumwallpaper,
      mylarwallpaper, naturaltextilewallpaper, pastelwallpaper, saloonwallpaper,
      textilewallpaper, wallpapersback).
    
    - server.js: new /api/site-products?site=<domain>&q=&limit= proxy.
      Validates site against an in-memory allowlist baked from sites.json at
      boot + on-file-change (fs.watchFile, 5s interval). TSD continues
      through its bespoke /api/catalog/search; everything else hits
      https://<site>/api/products with q/limit forwarded.
    
    - index.html: fetches /api/site-products?site=… for every wired site.
      Normalizes shape (DW sites return either wrapped {items} or bare array;
      TSD returns {total, items}). Absolute https:// image URLs (Shopify CDN)
      used directly — only /api/art-archive paths go through the /img proxy.
      Items with no image are now skipped instead of rendering as black tiles.
    
    Fixes Steve's complaint: clicking a site in the rail now loads real
    products from that site, not 'Catalog connection pending'. 20× the
    wired surface area in this revision.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/data/sites.json | 114 ++++++++++++++++++++++++++++++++++++++++---------
 public/index.html      |  25 ++++++++---
 server.js              |  56 ++++++++++++++++++++++++
 3 files changed, 170 insertions(+), 25 deletions(-)

diff --git a/public/data/sites.json b/public/data/sites.json
index 1bc5ba8..05f9591 100644
--- a/public/data/sites.json
+++ b/public/data/sites.json
@@ -79,25 +79,41 @@
       "domain": "corkwallcovering.com",
       "name": "corkwallcovering.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "silkwallpaper.com",
       "name": "silkwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "linenwallpaper.com",
       "name": "linenwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "chinoiseriewallpaper.com",
       "name": "chinoiseriewallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "glassbeadedwallpaper.com",
@@ -109,7 +125,11 @@
       "domain": "metallicwallpaper.com",
       "name": "metallicwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "90210rentals.com",
@@ -121,19 +141,31 @@
       "domain": "agedwallpaper.com",
       "name": "agedwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "apartmentwallpaper.com",
       "name": "apartmentwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "architecturalwallcoverings.com",
       "name": "architecturalwallcoverings.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "askgoodquestions.ai",
@@ -163,7 +195,11 @@
       "domain": "blockprintedwallpaper.com",
       "name": "blockprintedwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "boomercalc.com",
@@ -229,7 +265,11 @@
       "domain": "carmelwallpapers.com",
       "name": "carmelwallpapers.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "cfafabrics.com",
@@ -337,7 +377,11 @@
       "domain": "fabricwallpaper.com",
       "name": "fabricwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "fentucci.com",
@@ -391,7 +435,11 @@
       "domain": "greenwallcoverings.com",
       "name": "greenwallcoverings.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "hospitalitysalesagency.com",
@@ -433,13 +481,21 @@
       "domain": "museumwallpaper.com",
       "name": "museumwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "mylarwallpaper.com",
       "name": "mylarwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "nataliaabrams.com",
@@ -457,13 +513,21 @@
       "domain": "naturaltextilewallpaper.com",
       "name": "naturaltextilewallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "pastelwallpaper.com",
       "name": "pastelwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "patterndesignlab.com",
@@ -481,7 +545,11 @@
       "domain": "saloonwallpaper.com",
       "name": "saloonwallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "steveabramsdesigns.com",
@@ -541,7 +609,11 @@
       "domain": "textilewallpaper.com",
       "name": "textilewallpaper.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "theatrewallpaper.com",
@@ -595,7 +667,11 @@
       "domain": "wallpapersback.com",
       "name": "wallpapersback.com",
       "kind": "wallcovering",
-      "source": null
+      "source": {
+        "kind": "dw-products",
+        "endpoint": "/api/products",
+        "shape": "wrapped+url"
+      }
     },
     {
       "domain": "wholivedthere.com",
diff --git a/public/index.html b/public/index.html
index 2defb25..e52e831 100644
--- a/public/index.html
+++ b/public/index.html
@@ -482,16 +482,20 @@
       const token = ++queryToken;
       $count.textContent = 'loading…';
       const params = new URLSearchParams();
+      params.set('site', selectedSite.domain);
       if ($q.value) params.set('q', $q.value);
       params.set('limit', '120');
       try {
-        const r = await fetch(src.endpoint + '?' + params.toString());
+        const r = await fetch('/api/site-products?' + params.toString());
         const j = await r.json();
         if (token !== queryToken) return;
-        railItems = j.items || [];
-        $count.textContent = `${railItems.length} of ${(j.total || railItems.length).toLocaleString()}`;
+        // Normalize shape: TSD returns {total, items}, DW returns either {items} or [].
+        const items = Array.isArray(j) ? j : (j.items || []);
+        const total = (j && j.total) || items.length;
+        railItems = items;
+        $count.textContent = `${items.length} of ${total.toLocaleString()}`;
         renderGrid();
-      } catch (e) { $count.textContent = 'load failed'; }
+      } catch (e) { $count.textContent = 'load failed: ' + e.message; }
     }
 
     function renderGrid() {
@@ -506,8 +510,17 @@
         const div = document.createElement('div');
         div.className = 'card';
         div.draggable = true;
-        const imageProxy = '/img?path=' + encodeURIComponent(it.image_url || it.image || '');
-        const lite = { id: it.id, title: it.title, artist: it.artist, year: it.year, era: it.era, imageProxy };
+        const rawImg = it.image_url || it.image || '';
+        if (!rawImg) continue; // skip items with no image — keeps the grid clean
+        // Absolute https URLs (Shopify CDN, etc.) used directly; TSD /api/art-archive paths go through our /img proxy.
+        const imageProxy = /^https?:\/\//.test(rawImg) ? rawImg : ('/img?path=' + encodeURIComponent(rawImg));
+        const lite = {
+          id: it.id || it.sku || it.handle,
+          title: it.title || it.product_title || 'Untitled',
+          artist: it.artist || it.vendor || '',
+          year: it.year, era: it.era,
+          imageProxy,
+        };
         div.dataset.item = JSON.stringify(lite);
         const img = document.createElement('img');
         img.src = imageProxy; img.loading = 'lazy'; img.alt = it.title || '';
diff --git a/server.js b/server.js
index 94f5bdc..fa6e318 100644
--- a/server.js
+++ b/server.js
@@ -71,6 +71,62 @@ app.get('/api/catalog/artists', (req, res) => {
   proxyJson(req, res, '/api/catalog/artists');
 });
 
+// ── Generic site-products proxy: any DW microsite wired in sites.json ─────
+// /api/site-products?site=<domain>&q=<query>&limit=<n>
+// site MUST appear in public/data/sites.json with a source.endpoint and a
+// wallcovering/dw-shape kind. Validates against the allowlist baked at boot.
+const fs = require('fs');
+const sitesPath = path.join(__dirname, 'public', 'data', 'sites.json');
+let wiredSites = {};
+function loadWiredSites() {
+  try {
+    const data = JSON.parse(fs.readFileSync(sitesPath, 'utf8'));
+    wiredSites = {};
+    for (const s of (data.sites || [])) {
+      if (s.source && s.source.endpoint) {
+        wiredSites[s.domain] = s.source;
+      }
+    }
+    console.log('[4square] loaded wired sites:', Object.keys(wiredSites).length);
+  } catch (e) {
+    console.error('[4square] failed to load sites.json:', e.message);
+  }
+}
+loadWiredSites();
+fs.watchFile(sitesPath, { interval: 5000 }, loadWiredSites);
+
+app.get('/api/site-products', (req, res) => {
+  const site = String(req.query.site || '');
+  const cfg = wiredSites[site];
+  if (!cfg) return res.status(400).json({ ok: false, error: 'site not in wired allowlist: ' + site });
+  // TSD goes through its own proxy (different shape).
+  if (cfg.kind === 'tsd-catalog') {
+    const params = new URLSearchParams();
+    if (req.query.q) params.set('q', req.query.q);
+    if (req.query.limit) params.set('limit', req.query.limit);
+    return proxyJson(req, res, cfg.endpoint + '?' + params.toString());
+  }
+  // DW microsite: hit https://<site>/api/products?q=&limit=
+  const params = new URLSearchParams();
+  if (req.query.q) params.set('q', req.query.q);
+  params.set('limit', req.query.limit || '120');
+  const url = new URL('https://' + site + cfg.endpoint + '?' + params.toString());
+  const lib = url.protocol === 'https:' ? https : http;
+  const upstream = lib.request({
+    method: 'GET',
+    hostname: url.hostname,
+    port: 443,
+    path: url.pathname + url.search,
+    headers: { 'User-Agent': '4square-agentabrams/0.1', 'Accept': 'application/json' },
+  }, (r) => {
+    res.status(r.statusCode || 502);
+    res.set('Content-Type', r.headers['content-type'] || 'application/json');
+    r.pipe(res);
+  });
+  upstream.on('error', (e) => res.status(502).json({ ok: false, error: 'upstream ' + e.message }));
+  upstream.end();
+});
+
 // Image bytes — explicit path param so we don't accidentally proxy arbitrary URLs.
 app.get('/img', (req, res) => {
   const p = String(req.query.path || '');

← 2c34d92 v2 layout — site nav rail + adjustable-width product browser  ·  back to 4square Agentabrams  ·  yolo: add Mac2-local refinement loop script + 7-priority tas ca61c85 →