#!/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 += `

    ${pp.title}

    ${pp.desc}

    `; }); 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 += `
    ${item.a}
    `; }); 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(', ')); }