← back to Marketing Command Center
auto-save: 2026-07-17T09:46:05 (2 files) — modules/channels/index.js public/panels/composer.js
c3336242ce545b45e4bfdf82e5956b484be11dd8 · 2026-07-17 09:46:08 -0700 · Steve Abrams
Files touched
M modules/channels/index.jsM public/panels/composer.js
Diff
commit c3336242ce545b45e4bfdf82e5956b484be11dd8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 17 09:46:08 2026 -0700
auto-save: 2026-07-17T09:46:05 (2 files) — modules/channels/index.js public/panels/composer.js
---
modules/channels/index.js | 7 ++++++-
public/panels/composer.js | 10 ++++++++--
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/modules/channels/index.js b/modules/channels/index.js
index cc2d3a8..2d73b3f 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -700,9 +700,14 @@ module.exports = {
const d = req.body || {};
const channels = Array.isArray(d.channels) ? d.channels.filter(c => ADAPTERS[c]) : [];
const selectedPages = Array.isArray(d.pages) ? d.pages.map(String).filter(Boolean) : [];
- const content = { caption: (d.caption || '').slice(0, 2200), mediaUrl: d.mediaUrl || '' };
+ const content = { caption: (d.caption || '').slice(0, 2200), mediaUrl: d.mediaUrl || '', videoUrl: d.videoUrl || '' };
if (!channels.length) return res.status(400).json({ error: 'no channels selected' });
if (!content.caption && !content.mediaUrl) return res.status(400).json({ error: 'empty post' });
+ // HARD RULE (Steve 2026-07-17): every DW social post MUST carry a photo/video/reel —
+ // NEVER text-only. Block a text-only publish server-side (dry-run staging too).
+ if (!content.mediaUrl && !content.videoUrl) {
+ return res.status(400).json({ error: 'A photo, video, or reel is required — DW never posts text-only. Attach an image and try again.' });
+ }
const st = platformStatus();
const live = d.confirm === true && d.dryRun === false;
const outbox = readOutbox();
diff --git a/public/panels/composer.js b/public/panels/composer.js
index 0374d8a..90ab4c5 100644
--- a/public/panels/composer.js
+++ b/public/panels/composer.js
@@ -8,6 +8,7 @@ window.MCC_PANELS['composer'] = {
const jpost = async (u, b) => { try { const r = await fetch(O + u, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(b) }); return await r.json().catch(() => ({})); } catch (e) { return { error: e.message }; } };
const state = { mediaUrl: '', src: 'assets', selected: new Set() };
+ const NEW_ARRIVALS = 'designerwallcoverings.com/collections/new-arrivals'; // bare → clickable via facets
// ── RIGHT rail: publish targets, connection-aware ──────────────────────────
async function loadTargets() {
@@ -117,7 +118,10 @@ window.MCC_PANELS['composer'] = {
// 'social' silently degrades server-side to a 'headline' — the wrong content type.
const r = await jpost('/api/copy/generate', { kind: 'instagram', tone, topic: `${tone} wallcovering social post for Designer Wallcoverings`, product: 'any' });
const v = (r && r.variants) || [];
- const first = v.length ? (typeof v[0] === 'string' ? v[0] : (v[0].text || v[0].body || v[0].caption || '')) : '';
+ let first = v.length ? (typeof v[0] === 'string' ? v[0] : (v[0].text || v[0].body || v[0].caption || '')) : '';
+ // HARD RULE: always link back to DW new arrivals (the facet detector makes the bare
+ // domain clickable). Append it if the copy doesn't already reference the site.
+ if (first && !/designerwallcoverings\.com/i.test(first)) first += `\n\nShop new arrivals → ${NEW_ARRIVALS}`;
if (first) { cap.value = first; countIt(); }
$('#cmp-gen').disabled = false; $('#cmp-gen').textContent = '✨ Generate copy';
});
@@ -127,7 +131,9 @@ window.MCC_PANELS['composer'] = {
const out = $('#cmp-result'); out.innerHTML = '';
const channels = [...state.selected];
if (!channels.length) { out.innerHTML = '<span class="warn">Pick at least one connected channel.</span>'; return; }
- if (!cap.value.trim() && !state.mediaUrl) { out.innerHTML = '<span class="warn">Add a caption or an image first.</span>'; return; }
+ // HARD RULE: DW never posts text-only — a photo/video/reel is required.
+ if (!state.mediaUrl) { out.innerHTML = '<span class="warn">📷 A photo or video is required — DW never posts text-only. Pick an asset on the left first.</span>'; return; }
+ if (!cap.value.trim()) { out.innerHTML = '<span class="warn">Add a caption.</span>'; return; }
const dry = $('#cmp-dry').checked;
$('#cmp-publish').disabled = true; $('#cmp-publish').textContent = dry ? 'Staging…' : 'Publishing…';
const r = await jpost('/api/channels/publish', { channels, caption: cap.value.trim(), mediaUrl: state.mediaUrl, confirm: !dry, dryRun: dry });
← 19aea36 bluesky: rich-text FACETS — #hashtags + links now clickable
·
back to Marketing Command Center
·
cc-import: stage gated FM-client → Constant Contact import ( 47459ec →