← 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
M public/data/sites.jsonM public/index.htmlM server.js
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 →