[object Object]

← back to Norma

IG posts: 2-slide carousel — room-setting photo first, product swatch second (Steve 8/15); leak-guarded, single-image fallback when no room setting; cadence inherits it via post-to --product

d822b5890c557c2d20a361a0eaeb711817e265f4 · 2026-08-15 07:50:37 -0700 · Steve

Files touched

Diff

commit d822b5890c557c2d20a361a0eaeb711817e265f4
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Aug 15 07:50:37 2026 -0700

    IG posts: 2-slide carousel — room-setting photo first, product swatch second (Steve 8/15); leak-guarded, single-image fallback when no room setting; cadence inherits it via post-to --product
---
 agents/instagram-agent/content.js | 29 ++++++++++++++++++++++-------
 agents/instagram-agent/post-to.js | 10 ++++++++--
 2 files changed, 30 insertions(+), 9 deletions(-)

diff --git a/agents/instagram-agent/content.js b/agents/instagram-agent/content.js
index 4b9f952..55216a7 100644
--- a/agents/instagram-agent/content.js
+++ b/agents/instagram-agent/content.js
@@ -137,7 +137,23 @@ async function fetchProduct(url) {
   return fetch(url); // final attempt; caller handles a non-ok status
 }
 
-/** Resolve a product → { image_url, caption, title, url }. Throws if not found/imageless/leaky. */
+// Build the post's image order (Steve, 2026-08-15): ROOM-SETTING photo first, product swatch
+// second — as a 2-slide carousel. Only leak-clean images are eligible. Falls back to a single
+// product image when no room setting exists.
+const ROOM_RE = /room|setting|scene|lifestyle|install|bedroom|living|dining|display|interior/i;
+function pickImages(product) {
+  const imgs = (product.images || [])
+    .map((im) => ({ src: im.src, file: String(im.src || '').split('/').pop().split('?')[0], alt: im.alt || '' }))
+    .filter((x) => x.src && !leakToken(x.src));                 // leak-clean only
+  if (!imgs.length) return [];
+  const isRoom = (x) => ROOM_RE.test(x.file) || ROOM_RE.test(x.alt);
+  const room = imgs.find(isRoom);
+  const swatch = imgs.find((x) => x !== room && !isRoom(x)) || imgs.find((x) => x !== room) || imgs[0];
+  // room first, product swatch second; else just the swatch
+  return (room && swatch && room.src !== swatch.src) ? [room.src, swatch.src] : [swatch.src];
+}
+
+/** Resolve a product → { image_url, images[], caption, title, url }. Throws if not found/imageless/leaky. */
 async function resolveProduct(input) {
   const { handle, store } = handleFrom(input);
   const url = `${store}/products/${handle}.json`;
@@ -145,13 +161,12 @@ async function resolveProduct(input) {
   if (!r.ok) throw new Error(`product "${handle}" not found (${r.status}) at ${store}`);
   const { product } = await r.json();
   if (!product) throw new Error(`no product payload for "${handle}"`);
-  const img = (product.images && product.images[0] && product.images[0].src) || product.image?.src;
-  if (!img) throw new Error(`product "${handle}" has no image`);
-  // Private-label leak guard: the image URL and vendor go public — refuse if either leaks an upstream.
-  const leak = leakToken(img) || leakToken(product.vendor);
-  if (leak) throw new Error(`private-label leak (upstream "${leak}" in image/vendor) — skipped for compliance`);
+  if (leakToken(product.vendor)) throw new Error(`private-label leak (vendor "${leakToken(product.vendor)}") — skipped`);
+  const images = pickImages(product);
+  if (!images.length) throw new Error(`product "${handle}" has no clean image`);
   const productUrl = `${store}/products/${handle}`;
-  return { image_url: img, caption: autoCaption(product, productUrl), title: product.title, url: productUrl };
+  // image_url = the PRODUCT swatch (last slide) for single-image callers; images[] = full carousel order.
+  return { image_url: images[images.length - 1], images, caption: autoCaption(product, productUrl), title: product.title, url: productUrl };
 }
 
 module.exports = { resolveProduct, handleFrom };
diff --git a/agents/instagram-agent/post-to.js b/agents/instagram-agent/post-to.js
index 9d3554d..bf91c37 100644
--- a/agents/instagram-agent/post-to.js
+++ b/agents/instagram-agent/post-to.js
@@ -199,9 +199,15 @@ async function main() {
   if (args.product) {
     try {
       const prod = await content.resolveProduct(args.product);
-      args.image = args.image || prod.image_url;
+      // Carousel when a room-setting image exists (room first, product swatch second); else single image.
+      if (!args.image && !args.images && prod.images && prod.images.length > 1) {
+        args.images = prod.images.join(',');
+        console.log(`Product: ${prod.title}\n  carousel: ${prod.images.length} slides (room setting → product)\n`);
+      } else {
+        args.image = args.image || prod.image_url;
+        console.log(`Product: ${prod.title}\n  image: ${prod.image_url}\n`);
+      }
       if (!args.caption) args.caption = prod.caption;
-      console.log(`Product: ${prod.title}\n  image: ${prod.image_url}\n`);
       args._productTitle = prod.title;
       args._productHandle = String(args.product).replace(/^@/, '');
       args._imageUrl = prod.image_url;

← e654e89 IG cadence: exclude @beverlyhillsvideos (dedicated LIVE reel  ·  back to Norma  ·  IG: duplicate-image detector — md5-hash each post's image pe eb6bf21 →