← back to Consulting Intake

build.mjs

58 lines

#!/usr/bin/env node
// Consulting Intake — deliverable generator (fantasea-consulting pattern).
// Emits public/versions/<aesthetic>.html: ONE editorial template skinned by a
// palette/type/copy object per aesthetic direction, so the client picks a look.
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const HERE = dirname(fileURLToPath(import.meta.url));
const OUT = join(HERE, 'public', 'versions');
mkdirSync(OUT, { recursive: true });

const client = JSON.parse(readFileSync(join(HERE, 'data', 'client.json'), 'utf8'));

// Each aesthetic = a skin. Add/edit freely; the client chooses from these.
const SKINS = {
  editorial: { bg:'#f6f2ec', ink:'#14110e', accent:'#a9884f', font:"Georgia,serif", vibe:'Luxury / editorial' },
  modern:    { bg:'#ffffff', ink:'#0b0b0d', accent:'#2b6cff', font:"'Helvetica Neue',system-ui,sans-serif", vibe:'Modern / minimal' },
  bold:      { bg:'#0d0d12', ink:'#f5f5f7', accent:'#ff4d6d', font:"'Arial Black',system-ui,sans-serif", vibe:'Bold / energetic' },
  warm:      { bg:'#fbf3e9', ink:'#3a2a1c', accent:'#d98a3d', font:"'Georgia',serif", vibe:'Warm / approachable' },
  classic:   { bg:'#f4f4f0', ink:'#1c2530', accent:'#7a5b34', font:"'Times New Roman',serif", vibe:'Classic / trustworthy' },
};

const esc = (s) => String(s || '').replace(/[&<>]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[c]));
const goals = (client.goals || []).slice(0, 4);

for (const [name, s] of Object.entries(SKINS)) {
  const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(client.business_name)} — ${s.vibe}</title>
<style>
:root{--bg:${s.bg};--ink:${s.ink};--accent:${s.accent}}
*{box-sizing:border-box}body{margin:0;font-family:${s.font};background:var(--bg);color:var(--ink)}
.bar{padding:14px 30px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--accent)}
.bar b{letter-spacing:.06em}.bar span{color:var(--accent);font-size:12px;text-transform:uppercase;letter-spacing:.2em}
.hero{padding:90px 30px;text-align:center}
.hero .k{color:var(--accent);letter-spacing:.3em;text-transform:uppercase;font-size:12px}
.hero h1{font-size:56px;margin:.2em 0;font-weight:700}
.hero p{font-size:20px;max-width:620px;margin:0 auto;opacity:.8}
.cta{display:inline-block;margin-top:26px;background:var(--accent);color:var(--bg);padding:14px 32px;text-decoration:none;text-transform:uppercase;letter-spacing:.16em;font-size:13px}
.goals{max-width:900px;margin:40px auto;padding:0 30px;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:18px}
.goals div{border:1px solid var(--accent);padding:22px;font-size:17px}
.foot{padding:40px 30px;text-align:center;opacity:.6;font-size:13px}
</style></head><body>
<div class="bar"><b>${esc(client.business_name)}</b><span>${s.vibe} concept</span></div>
<div class="hero">
  <div class="k">${esc(client.industry || '')} ${client.location ? '· ' + esc(client.location) : ''}</div>
  <h1>${esc(client.business_name)}</h1>
  <p>${esc(client.tagline || 'A new website concept, ready to make your first impression unforgettable.')}</p>
  <a class="cta" href="#">Get in touch</a>
</div>
${goals.length ? '<div class="goals">' + goals.map(g => '<div>' + esc(g) + '</div>').join('') + '</div>' : ''}
<div class="foot">Concept "${name}" · generated ${client.generated_at || ''} · one of several directions to choose from.</div>
</body></html>`;
  writeFileSync(join(OUT, `${name}.html`), html);
}
console.log('Generated ' + Object.keys(SKINS).length + ' concept versions into public/versions/');