#!/usr/bin/env node
/**
* MCPEngage Landing Page Generator
* Takes the gold-standard Zendesk v2 template and generates per-platform pages
* from config objects.
*
* Usage: node generate.js [platform-slug] or node generate.js --all
*/
const fs = require('fs');
const path = require('path');
const CONFIGS_DIR = path.join(__dirname, 'configs');
const OUTPUT_DIR = path.join(__dirname, '..', 'mcpengage-deploy');
const TEMPLATE_PATH = path.join(__dirname, 'template.html');
function loadTemplate() {
return fs.readFileSync(TEMPLATE_PATH, 'utf8');
}
function loadConfig(slug) {
const p = path.join(CONFIGS_DIR, `${slug}.json`);
if (!fs.existsSync(p)) throw new Error(`Config not found: ${slug}`);
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
function getAllConfigs() {
return fs.readdirSync(CONFIGS_DIR)
.filter(f => f.endsWith('.json'))
.map(f => f.replace('.json', ''));
}
/**
* Replace all {{PLACEHOLDER}} tokens in the template with config values.
* Supports nested: {{chat.userMsg1}}, arrays via {{#features}}...{{/features}}
*/
function renderTemplate(template, config) {
let html = template;
// Simple replacements: {{key}}
const simpleKeys = [
'name', 'slug', 'tagline', 'metaDescription', 'heroHeadline',
'heroSubtitle', 'badgeText', 'toolCount', 'urlBarPath',
'installPlatformName', 'installToolCount',
'ctaHeadline', 'ctaSubtext',
'painPointsHeadline', 'painPointsSubHeadline',
'howItWorksHeadline',
'featuresHeadline', 'featuresSubtext',
];
for (const key of simpleKeys) {
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
html = html.replace(regex, config[key] || '');
}
// Sidebar icons (array of {icon, label})
if (config.sidebarIcons) {
let sidebarHtml = '';
config.sidebarIcons.forEach((item, i) => {
const active = i === 0;
sidebarHtml += `
`;
});
html = html.replace('{{sidebarIcons}}', sidebarHtml);
}
// Right panel tabs
if (config.rightPanel) {
const rp = config.rightPanel;
html = html.replace(/\{\{rightPanel\.tab1Label\}\}/g, rp.tab1Label || 'Items');
html = html.replace(/\{\{rightPanel\.tab1Icon\}\}/g, rp.tab1Icon || 'list');
html = html.replace(/\{\{rightPanel\.tab1Count\}\}/g, rp.tab1Count || '12');
html = html.replace(/\{\{rightPanel\.tab2Label\}\}/g, rp.tab2Label || 'Details');
html = html.replace(/\{\{rightPanel\.tab2Icon\}\}/g, rp.tab2Icon || 'user');
// Right panel cards
let cardsHtml = '';
(rp.cards || []).forEach(card => {
cardsHtml += `
${card.title}
${card.subtitle}
${card.badge}
${card.tags.map(t => `${t.text}`).join('\n ')}
`;
});
html = html.replace('{{rightPanel.cards}}', cardsHtml);
}
// Chat input placeholder
html = html.replace(/\{\{chatPlaceholder\}\}/g, config.chatPlaceholder || `Ask about your ${config.name}...`);
// Stats
if (config.stats) {
let statsHtml = '';
const colors = ['brand', 'cyan', 'violet', 'emerald'];
config.stats.forEach((stat, i) => {
const c = colors[i % colors.length];
const isCounter = stat.value !== undefined && !isNaN(stat.value);
statsHtml += `
${stat.prefix || ''}${isCounter ? `0` : stat.display}${stat.suffix || ''}
${stat.label}
`;
});
html = html.replace('{{statsGrid}}', statsHtml);
}
// Before items
if (config.beforeItems) {
let bHtml = '';
const icons = ['clock', 'copy', 'layout-grid', 'hourglass'];
config.beforeItems.forEach((item, i) => {
bHtml += `
${item.title}
${item.desc}
`;
});
html = html.replace('{{beforeItems}}', bHtml);
}
// After items
if (config.afterItems) {
let aHtml = '';
const icons = ['zap', 'sparkles', 'message-square', 'rocket'];
config.afterItems.forEach((item, i) => {
aHtml += `
${item.title}
${item.desc}
`;
});
html = html.replace('{{afterItems}}', aHtml);
}
// Pain points
if (config.painPoints) {
let ppHtml = '';
const ppIcons = ['layers', 'search-x', 'timer-off'];
config.painPoints.forEach((pp, i) => {
const colSpan = i === 2 ? 'sm:col-span-2 lg:col-span-1' : '';
ppHtml += `
`;
});
html = html.replace('{{painPointCards}}', ppHtml);
}
// How it works steps
if (config.howItWorks) {
let hiwHtml = '';
const gradients = [
'from-brand-500 to-teal-400', 'from-cyan-500 to-blue-400', 'from-violet-500 to-purple-400'
];
const shadows = ['brand', 'cyan', 'violet'];
config.howItWorks.forEach((step, i) => {
hiwHtml += `
${i + 1}
${step.title}
${step.desc}
`;
});
html = html.replace('{{howItWorksSteps}}', hiwHtml);
}
// Features grid
if (config.features) {
let fHtml = '';
const fColors = ['brand', 'cyan', 'violet', 'orange', 'pink', 'emerald'];
const fGradients = [
'from-brand-500/20 to-teal-500/20',
'from-cyan-500/20 to-blue-500/20',
'from-violet-500/20 to-purple-500/20',
'from-orange-500/20 to-red-500/20',
'from-pink-500/20 to-rose-500/20',
'from-emerald-500/20 to-teal-500/20',
];
config.features.forEach((feat, i) => {
const c = fColors[i % fColors.length];
const g = fGradients[i % fGradients.length];
fHtml += `
${feat.title}
${feat.desc}
`;
});
html = html.replace('{{featuresGrid}}', fHtml);
}
// FAQ items
if (config.faq) {
let faqHtml = '';
config.faq.forEach(item => {
faqHtml += `
`;
});
html = html.replace('{{faqItems}}', faqHtml);
}
// Chat messages (the animated conversation)
if (config.chatMessages) {
// Build the embeds and messages JS
let embedsJs = '';
let messagesJs = 'const messages = [\n';
config.chatMessages.forEach((msg, i) => {
if (msg.embed) {
embedsJs += ` const embed_${i} = \`${msg.embed}\`;\n`;
}
messagesJs += ` { type:'${msg.type}', text:'${msg.text.replace(/'/g, "\\'")}'${msg.embed ? `, embed: embed_${i}` : ''} },\n`;
});
messagesJs += ' ];';
html = html.replace('{{chatEmbedsJs}}', embedsJs);
html = html.replace('{{chatMessagesJs}}', messagesJs);
}
// Terminal lines
if (config.terminalLines) {
let termJs = 'const termLines = [\n';
config.terminalLines.forEach(line => {
termJs += ` { text:'${line.text.replace(/'/g, "\\'")}', color:'${line.color || 'text-white'}', delay:${line.delay || 0} },\n`;
});
termJs += ' ];';
html = html.replace('{{terminalLinesJs}}', termJs);
}
return html;
}
function generate(slug) {
const template = loadTemplate();
const config = loadConfig(slug);
const html = renderTemplate(template, config);
// Write to output directory
const outDir = path.join(OUTPUT_DIR, slug);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'index.html'), html);
console.log(`ā Generated: ${slug}/index.html`);
return outDir;
}
// CLI
const args = process.argv.slice(2);
if (args.includes('--all')) {
const slugs = getAllConfigs();
console.log(`Generating ${slugs.length} landing pages...`);
slugs.forEach(slug => generate(slug));
console.log(`\nā Done! ${slugs.length} pages generated in ${OUTPUT_DIR}`);
} else if (args[0]) {
generate(args[0]);
} else {
console.log('Usage: node generate.js [slug] or node generate.js --all');
console.log('Available configs:', getAllConfigs().join(', '));
}