← back to Interiordesignershowroom
IDS admin: implement EZ-Join DTD verdict (C+relabel), fix smart-quote regression, sync rooms.hotspots schema (v0.2.1)
3e86ee5dfc6aa3ca83d76a6c47d505fb2846c261 · 2026-08-03 09:49:33 -0700 · Steve
- EZ-Join verdict C-as-superset-of-A: joinTarget() resolves a per-network
marketplace SEARCH deep-link (brand pre-filled) where searchVerified, else the
plain landing (safe fallback). All networks land today (unverified); flip
searchVerified per network after a live test. Honest relabel 'EZ Join' ->
'Join via <Network>'.
- Fix: refactor pass had introduced curly quotes as HTML attr delimiters on the
join button (class="ezjoin"/href broke styling+link) — restored straight quotes.
- db/rooms.sql: declare + idempotently ALTER-add the hotspots JSONB column that
createRoom() writes (schema file was drifted from the live DB).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M db/rooms.sqlM package-lock.jsonM package.jsonM routes/admin.js
Diff
commit 3e86ee5dfc6aa3ca83d76a6c47d505fb2846c261
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 3 09:49:33 2026 -0700
IDS admin: implement EZ-Join DTD verdict (C+relabel), fix smart-quote regression, sync rooms.hotspots schema (v0.2.1)
- EZ-Join verdict C-as-superset-of-A: joinTarget() resolves a per-network
marketplace SEARCH deep-link (brand pre-filled) where searchVerified, else the
plain landing (safe fallback). All networks land today (unverified); flip
searchVerified per network after a live test. Honest relabel 'EZ Join' ->
'Join via <Network>'.
- Fix: refactor pass had introduced curly quotes as HTML attr delimiters on the
join button (class="ezjoin"/href broke styling+link) — restored straight quotes.
- db/rooms.sql: declare + idempotently ALTER-add the hotspots JSONB column that
createRoom() writes (schema file was drifted from the live DB).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
db/rooms.sql | 3 +++
package-lock.json | 4 ++--
package.json | 2 +-
routes/admin.js | 48 +++++++++++++++++++++++++++++++++++-------------
4 files changed, 41 insertions(+), 16 deletions(-)
diff --git a/db/rooms.sql b/db/rooms.sql
index 08ac8ae..1f87f39 100644
--- a/db/rooms.sql
+++ b/db/rooms.sql
@@ -15,9 +15,12 @@ CREATE TABLE IF NOT EXISTS rooms (
created_by TEXT DEFAULT 'visitor', -- 'curator' (featured) | 'visitor'
featured BOOLEAN DEFAULT FALSE,
public BOOLEAN DEFAULT TRUE,
+ hotspots JSONB NOT NULL DEFAULT '[]', -- [{id,box:{x,y,w,h},...}] shoppable pins on the scene
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
+-- Existing DBs predating the column: add it idempotently (createRoom() writes hotspots).
+ALTER TABLE rooms ADD COLUMN IF NOT EXISTS hotspots JSONB NOT NULL DEFAULT '[]';
CREATE INDEX IF NOT EXISTS idx_rooms_featured ON rooms (featured, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_rooms_style ON rooms (style);
diff --git a/package-lock.json b/package-lock.json
index 72ddafa..1e1c124 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "interiordesignershowroom",
- "version": "0.2.0",
+ "version": "0.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "interiordesignershowroom",
- "version": "0.2.0",
+ "version": "0.2.1",
"dependencies": {
"dotenv": "^17.4.2",
"express": "^4.19.2",
diff --git a/package.json b/package.json
index 556bf61..21564f7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "interiordesignershowroom",
- "version": "0.2.0",
+ "version": "0.2.1",
"private": true,
"description": "Curated, editorial affiliate showroom for interior design — multi-network (CJ, Amazon, Rakuten, ShareASale) feed-driven catalog + buying guides.",
"main": "server.js",
diff --git a/routes/admin.js b/routes/admin.js
index 7b4b727..4968b83 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -13,16 +13,34 @@ const router = express.Router();
// Affiliate networks we can send Steve to in order to JOIN a program. There is no
// true one-click affiliate join (every network makes you log in as a publisher and
-// click "Join" on the advertiser), so "EZ Join" deep-links straight into that
-// network's marketplace — one search-and-click away, not a fabricated brand URL.
+// click "Join" on the advertiser). Per the DTD verdict (2026-08-03) the "Join via
+// <Network>" button resolves option C-as-a-superset-of-A: use a per-network
+// marketplace SEARCH deep-link (brand pre-filled, `{q}` placeholder) ONLY where that
+// pattern is verified to survive the network's login redirect (searchVerified:true);
+// everywhere else fall back to the plain marketplace `join` landing. Networks stay on
+// the landing until each `search` URL is confirmed (CJ's is login-gated and unverified,
+// so it lands — exactly the dissent's concern). To enable a search deep-link after a
+// 5-second live test, just flip that network's `searchVerified` to true.
const NETWORKS = {
- cj: { label: 'CJ', join: 'https://members.cj.com/member/publisher/marketplace' },
- rakuten: { label: 'Rakuten', join: 'https://rakutenadvertising.com/en-us/affiliate-marketing-for-publishers/' },
- shareasale: { label: 'ShareASale', join: 'https://account.shareasale.com/a-loginpage.cfm' },
- impact: { label: 'Impact', join: 'https://app.impact.com/' },
- amazon: { label: 'Amazon', join: 'https://affiliate-program.amazon.com/' },
+ cj: { label: 'CJ', join: 'https://members.cj.com/member/publisher/marketplace', search: 'https://members.cj.com/member/publisher/marketplace?keyword={q}', searchVerified: false },
+ rakuten: { label: 'Rakuten', join: 'https://rakutenadvertising.com/en-us/affiliate-marketing-for-publishers/', search: null, searchVerified: false },
+ shareasale: { label: 'ShareASale', join: 'https://account.shareasale.com/a-loginpage.cfm', search: 'https://account.shareasale.com/a-searchmerchants.cfm?keyword={q}', searchVerified: false },
+ impact: { label: 'Impact', join: 'https://app.impact.com/', search: null, searchVerified: false },
+ amazon: { label: 'Amazon', join: 'https://affiliate-program.amazon.com/', search: null, searchVerified: false },
};
+// Resolve the join target for a (network, brand): a brand-prefilled marketplace SEARCH
+// where that network has a verified search pattern, else the plain marketplace landing
+// (the safe A-fallback that never 404s). Returns { url, prefilled }.
+function joinTarget(network, brand) {
+ const net = NETWORKS[network];
+ if (!net) return { url: '#', prefilled: false };
+ if (net.searchVerified && net.search && brand) {
+ return { url: net.search.replace('{q}', encodeURIComponent(brand)), prefilled: true };
+ }
+ return { url: net.join, prefilled: false };
+}
+
// Load the curated suggestion roster (data/suggested-affiliates.json). Read live so
// the list can be extended without a restart; never throws the request on a bad file.
function loadSuggestions() {
@@ -569,7 +587,7 @@ router.get('/admin/affiliates', async (_req, res, next) => {
// Suggested lines to join — a curated gap-finder. Shows on-brand affiliate programs
// across CJ + other networks, flags the ones already in the catalog (✓ Joined), and
-// gives an "EZ Join" deep-link into the right network's marketplace for the rest.
+// gives a "Join via <Network>" deep-link into the right network's marketplace for the rest.
router.get('/admin/affiliates/suggested', async (req, res, next) => {
try {
const lines = loadSuggestions();
@@ -610,9 +628,13 @@ router.get('/admin/affiliates/suggested', async (req, res, next) => {
const rows = shown.map((l) => {
const primary = l.network;
const joinedLabel = l.joinedNets.map((k) => (NETWORKS[k] ? NETWORKS[k].label : k)).join(', ');
+ const t = joinTarget(primary, l.name);
+ const tip = t.prefilled
+ ? `Opens ${NETWORKS[primary].label} marketplace search pre-filled for "${l.name}" — click Join Program`
+ : `Opens ${NETWORKS[primary].label} — search "${l.name}" and click Join Program`;
const joinBtn = l.joined
- ? `<span class=”subtle” title=”Already in your catalog on ${esc(joinedLabel)}”>on ${esc(joinedLabel)}</span>`
- : `<a class=”ezjoin” href=”${esc(NETWORKS[primary].join)}” target=”_blank” rel=”noopener nofollow” title=”Opens ${esc(NETWORKS[primary].label)} — search “${esc(l.name)}” and click Join”>EZ Join on ${esc(NETWORKS[primary].label)} →</a>`;
+ ? `<span class="subtle" title="Already in your catalog on ${esc(joinedLabel)}">on ${esc(joinedLabel)}</span>`
+ : `<a class="ezjoin" href="${esc(t.url)}" target="_blank" rel="noopener nofollow" title="${esc(tip)}">Join via ${esc(NETWORKS[primary].label)} →</a>`;
return `<tr class="${l.joined ? 'row-off' : ''}">
<td><b>${esc(l.name)}</b></td>
<td>${netBadges(l.nets)}</td>
@@ -626,9 +648,9 @@ router.get('/admin/affiliates/suggested', async (req, res, next) => {
const intro = `<p class="subtle" style="margin:0 0 10px">
On-brand affiliate programs worth adding. <b>${openCount}</b> not yet joined ·
<b>${joinedCount}</b> already in your catalog. There's no true one-click join —
- <b>EZ Join</b> opens the network's publisher marketplace so you can search the brand
- and hit <i>Join Program</i> in one step. Extend the list in
- <code>data/suggested-affiliates.json</code>.</p>`;
+ <b>Join via <Network></b> drops you into that network's publisher marketplace
+ (pre-filled search where the network supports it) so you can hit <i>Join Program</i>.
+ Extend the list in <code>data/suggested-affiliates.json</code>.</p>`;
res.send(shell(`
<h1 style="font-size:1.15rem;margin:0 0 4px">Affiliates</h1>
← 562d723 chore: lint, refactor, v0.2.0 (session close)
·
back to Interiordesignershowroom
·
seo: indexable facet landing pages — sitemap + self-canonica 6a15502 →