← back to New Arrivals Content Engine
scripts/draft-social.cjs
101 lines
#!/usr/bin/env node
/**
* New-Arrivals Content Engine — STEP 2: draft social posts (LOCAL, $0, NO POST).
*
* Reads data/net-new-latest.json (from diff-new-arrivals.cjs), picks the top
* PICK net-new products (prefers ones WITH a featured image), and writes a
* DRAFT post package per product into data/review-queue/ — one JSON file each,
* status:"draft". Captions are generated locally from tag-derived style/color
* vocabulary (no paid API, no LLM call), and are Wallcovering-safe (no medical/
* performance/superlative claims that trip the outbound-comms gate).
*
* HARD RAIL: this NEVER posts to any platform and never schedules a live send.
* Publishing is Steve-gated. The room-render/reels step is left as an OPTIONAL
* $0-local hook (render_hint) the reviewer can trigger via reels-producer.
*
* Usage: node draft-social.cjs [--pick 5]
*/
const fs = require('fs');
const path = require('path');
const args = process.argv.slice(2);
const PICK = parseInt(args.find((_, i, a) => a[i - 1] === '--pick') || '5', 10) || 5;
const ROOT = path.join(__dirname, '..');
const IN = path.join(ROOT, 'data', 'net-new-latest.json');
const QUEUE = path.join(ROOT, 'data', 'review-queue');
const LOG = path.join(ROOT, 'data', 'engine.log');
if (!fs.existsSync(IN)) { console.error('Run diff-new-arrivals.cjs first — no net-new-latest.json'); process.exit(1); }
const data = JSON.parse(fs.readFileSync(IN, 'utf8'));
const stamp = () => new Date().toISOString().replace('T', ' ').slice(0, 19);
const log = (m) => { const l = `[${stamp()}] ${m}`; console.log(l); try { fs.appendFileSync(LOG, l + '\n'); } catch (_) {} };
// --- local vocab: derive a style + color read from tags/title (deterministic) ---
const STYLE_WORDS = ['Traditional', 'Modern', 'Floral', 'Damask', 'Geometric', 'Grasscloth',
'Botanical', 'Stripe', 'Toile', 'Chinoiserie', 'Textured', 'Metallic', 'Abstract'];
const COLOR_WORDS = ['Alabaster', 'Oatmeal', 'Greige', 'Celadon', 'Indigo', 'Charcoal', 'Blush',
'Emerald', 'Navy', 'Gold', 'Ivory', 'Sage', 'Terracotta', 'Slate', 'Cream'];
function pickWord(list, hay) {
const h = hay.toLowerCase();
return list.find((w) => h.includes(w.toLowerCase())) || null;
}
function caption(p) {
const hay = `${p.title} ${p.tags.join(' ')} ${p.type}`;
const style = pickWord(STYLE_WORDS, hay);
const color = pickWord(COLOR_WORDS, hay);
const kind = (p.type || 'Wallcovering').replace(/s$/, '');
const lead = `New arrival: ${p.title}`;
const desc = [
style ? `${style.toLowerCase()} ${kind.toLowerCase()}` : `${kind.toLowerCase()}`,
color ? `in a ${color.toLowerCase()} palette` : null,
].filter(Boolean).join(' ');
const base = `${lead} — a ${desc}, just added at Designer Wallcoverings.`;
const tagset = ['#wallcovering', '#interiordesign', '#designerwallcoverings',
style ? `#${style.toLowerCase()}` : null, '#newarrival'].filter(Boolean);
return {
instagram: `${base}\nSwatch samples available. Link in bio.\n${tagset.join(' ')}`,
tiktok: `${lead} ✨ ${desc}. #wallcovering #interiordesign #fyp`,
pinterest: { title: `${p.title} — ${style || kind}`, description: base },
derived: { style, color, kind },
};
}
function slug(s) { return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60); }
// prefer products with an image; keep created order otherwise
const ranked = [...data.net_new].sort((a, b) => (b.image ? 1 : 0) - (a.image ? 1 : 0));
const picks = ranked.slice(0, PICK);
fs.mkdirSync(QUEUE, { recursive: true });
let written = 0;
for (const p of picks) {
const cap = caption(p);
const numericId = String(p.id).split('/').pop();
const draft = {
status: 'draft', // NEVER auto-posts; reviewer flips to approved
created: stamp(),
date: data.date,
product: { id: p.id, title: p.title, handle: p.handle, vendor: p.vendor, type: p.type, image: p.image },
product_url: `https://www.designerwallcoverings.com/products/${p.handle}`,
captions: cap,
platforms: ['instagram', 'tiktok', 'pinterest'],
asset: {
featured_image: p.image,
render_hint: p.image
? `reels-producer: $0 local slideshow/room-render from ${p.image}`
: 'no featured image — needs asset before publish',
render_status: 'pending', // reviewer triggers reels-producer if wanted
},
gate: 'PUBLISH IS STEVE-GATED — this file is a DRAFT only. No send performed.',
};
const fp = path.join(QUEUE, `${data.date}__${slug(p.title || numericId)}.json`);
fs.writeFileSync(fp, JSON.stringify(draft, null, 2));
written++;
}
log(`drafted ${written} DRAFT post packages into ${QUEUE} (net-new pool=${data.net_new_count}, no posts sent)`);
console.log(`\n${written} draft(s) written to data/review-queue/ — review, then approve to publish (gated).`);