277 lines
12 KiB
JavaScript
277 lines
12 KiB
JavaScript
#!/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 += `
|
|
<div class="w-9 h-9 lg:w-10 lg:h-10 rounded-xl ${active ? 'bg-brand-500/20' : 'hover:bg-white/5'} flex items-center justify-center ${active ? 'text-brand-400' : 'text-zinc-600'} transition-colors">
|
|
<i data-lucide="${item.icon}" class="w-4 h-4 lg:w-5 lg:h-5"></i>
|
|
</div>`;
|
|
});
|
|
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 += `
|
|
<div class="glass rounded-xl xl:rounded-2xl p-4 xl:p-5 hover:bg-white/5 transition-all cursor-pointer group border border-transparent hover:border-brand-500/30">
|
|
<div class="flex items-start justify-between mb-2 xl:mb-3">
|
|
<div>
|
|
<h4 class="font-bold text-sm xl:text-base group-hover:text-brand-400 transition-colors">${card.title}</h4>
|
|
<p class="text-[10px] xl:text-xs text-zinc-500">${card.subtitle}</p>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-md bg-${card.badgeColor}-500/20 text-${card.badgeColor}-400 text-[10px] xl:text-xs font-bold">${card.badge}</span>
|
|
</div>
|
|
<div class="flex gap-1.5 xl:gap-2">
|
|
${card.tags.map(t => `<span class="px-2 xl:px-2.5 py-0.5 xl:py-1 rounded-md xl:rounded-lg ${t.bg || 'bg-brand-500/15'} ${t.color || 'text-brand-400'} text-[10px] xl:text-xs font-medium">${t.text}</span>`).join('\n ')}
|
|
</div>
|
|
</div>`;
|
|
});
|
|
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 += `
|
|
<div class="stat-card text-center">
|
|
<div class="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-black text-${c}-400 mb-1 sm:mb-2">${stat.prefix || ''}${isCounter ? `<span class="counter" data-target="${stat.value}">0</span>` : stat.display}${stat.suffix || ''}</div>
|
|
<div class="text-xs sm:text-sm text-zinc-500 font-medium">${stat.label}</div>
|
|
</div>`;
|
|
});
|
|
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 += `
|
|
<li class="flex items-start gap-3"><div class="w-6 h-6 rounded-full bg-red-500/10 flex items-center justify-center flex-shrink-0 mt-0.5"><i data-lucide="${icons[i % icons.length]}" class="w-3.5 h-3.5 text-red-400"></i></div><div><p class="text-sm sm:text-base text-zinc-300 font-medium">${item.title}</p><p class="text-xs sm:text-sm text-zinc-500">${item.desc}</p></div></li>`;
|
|
});
|
|
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 += `
|
|
<li class="flex items-start gap-3"><div class="w-6 h-6 rounded-full bg-brand-500/10 flex items-center justify-center flex-shrink-0 mt-0.5"><i data-lucide="${icons[i % icons.length]}" class="w-3.5 h-3.5 text-brand-400"></i></div><div><p class="text-sm sm:text-base text-zinc-300 font-medium">${item.title}</p><p class="text-xs sm:text-sm text-zinc-500">${item.desc}</p></div></li>`;
|
|
});
|
|
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 += `
|
|
<div class="glass rounded-2xl lg:rounded-3xl p-5 sm:p-6 lg:p-8 border border-red-500/10 hover:border-red-500/30 transition-all group feature-card ${colSpan} stagger-item">
|
|
<div class="w-12 h-12 sm:w-14 sm:h-14 lg:w-16 lg:h-16 rounded-xl lg:rounded-2xl bg-red-500/10 flex items-center justify-center mb-4 sm:mb-5 lg:mb-6 feature-icon">
|
|
<i data-lucide="${pp.icon || ppIcons[i]}" class="w-6 h-6 sm:w-7 sm:h-7 lg:w-8 lg:h-8 text-red-400"></i>
|
|
</div>
|
|
<h3 class="font-bold text-lg sm:text-xl lg:text-2xl mb-2 sm:mb-3 lg:mb-4">${pp.title}</h3>
|
|
<p class="text-zinc-400 text-sm sm:text-base lg:text-lg leading-relaxed">${pp.desc}</p>
|
|
</div>`;
|
|
});
|
|
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 += `
|
|
<div class="flex flex-col lg:flex-row items-center gap-4 sm:gap-6 lg:gap-10 glass rounded-2xl lg:rounded-3xl p-5 sm:p-6 lg:p-10 feature-card stagger-item">
|
|
<div class="w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 rounded-2xl lg:rounded-3xl bg-gradient-to-br ${gradients[i]} flex items-center justify-center flex-shrink-0 shadow-2xl shadow-${shadows[i]}-500/30">
|
|
<span class="text-3xl sm:text-4xl lg:text-5xl font-black text-black">${i + 1}</span>
|
|
</div>
|
|
<div class="text-center lg:text-left">
|
|
<h3 class="text-xl sm:text-2xl lg:text-3xl font-black mb-2 sm:mb-3 lg:mb-4">${step.title}</h3>
|
|
<p class="text-zinc-400 text-sm sm:text-base lg:text-xl leading-relaxed">${step.desc}</p>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
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 += `
|
|
<div class="glass rounded-2xl lg:rounded-3xl p-5 sm:p-6 lg:p-8 feature-card stagger-item group">
|
|
<div class="w-11 h-11 sm:w-12 sm:h-12 lg:w-14 lg:h-14 rounded-xl lg:rounded-2xl bg-gradient-to-br ${g} flex items-center justify-center mb-4 sm:mb-5 lg:mb-6 feature-icon">
|
|
<i data-lucide="${feat.icon}" class="w-5 h-5 sm:w-6 sm:h-6 lg:w-7 lg:h-7 text-${c}-400"></i>
|
|
</div>
|
|
<h3 class="font-bold text-base sm:text-lg lg:text-xl mb-2 sm:mb-3 group-hover:text-${c}-400 transition-colors">${feat.title}</h3>
|
|
<p class="text-zinc-400 text-sm sm:text-base">${feat.desc}</p>
|
|
</div>`;
|
|
});
|
|
html = html.replace('{{featuresGrid}}', fHtml);
|
|
}
|
|
|
|
// FAQ items
|
|
if (config.faq) {
|
|
let faqHtml = '';
|
|
config.faq.forEach(item => {
|
|
faqHtml += `
|
|
<div class="faq-item glass rounded-2xl overflow-hidden feature-card stagger-item">
|
|
<button class="faq-toggle w-full flex items-center justify-between p-5 sm:p-6 text-left" onclick="toggleFaq(this)">
|
|
<span class="font-bold text-sm sm:text-base lg:text-lg pr-4">${item.q}</span>
|
|
<i data-lucide="chevron-down" class="w-5 h-5 text-zinc-400 faq-chevron flex-shrink-0"></i>
|
|
</button>
|
|
<div class="faq-answer px-5 sm:px-6 text-zinc-400 text-sm sm:text-base leading-relaxed">
|
|
${item.a}
|
|
</div>
|
|
</div>`;
|
|
});
|
|
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(', '));
|
|
}
|