← 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
M agents/instagram-agent/content.jsM agents/instagram-agent/post-to.js
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 →