- Fix competitive-teardown/SKILL.md: replace broken references DATA_COLLECTION.md → references/data-collection-guide.md and TEMPLATES.md → references/analysis-templates.md (workflow was broken at steps 2 and 4) - Upgrade landing_page_scaffolder.py: add TSX + Tailwind output format (--format tsx) matching SKILL.md promise of Next.js/React components. 4 design styles (dark-saas, clean-minimal, bold-startup, enterprise). TSX is now default; HTML preserved via --format html - Rewrite README.md: fix stale counts (was 5 skills/15+ tools, now accurately shows 8 skills/9 tools), remove 7 ghost scripts that never existed (sprint_planner.py, velocity_tracker.py, etc.) - Fix tech-debt.md description (score → prioritize) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
569 lines
24 KiB
Python
569 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Landing Page Scaffolder — Generate landing pages as HTML or Next.js TSX from config.
|
|
|
|
Creates production-ready landing pages with hero sections, features,
|
|
testimonials, pricing, CTAs, and responsive design.
|
|
|
|
Usage:
|
|
python landing_page_scaffolder.py config.json --format html --output page.html
|
|
python landing_page_scaffolder.py config.json --format tsx --output LandingPage.tsx
|
|
python landing_page_scaffolder.py config.json --format json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from typing import Dict, List, Any, Optional
|
|
from datetime import datetime
|
|
import html as html_module
|
|
|
|
|
|
def escape(text: str) -> str:
|
|
"""HTML-escape text."""
|
|
return html_module.escape(str(text))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tailwind style mappings for TSX output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DESIGN_STYLES = {
|
|
"dark-saas": {
|
|
"bg": "bg-gray-950", "text": "text-white",
|
|
"accent": "violet", "card_bg": "bg-gray-900 border border-gray-800",
|
|
"btn": "bg-violet-600 hover:bg-violet-500 text-white",
|
|
"btn_secondary": "border border-gray-700 text-gray-300 hover:bg-gray-800",
|
|
"section_alt": "bg-gray-900/50", "muted": "text-gray-400",
|
|
"border": "border-gray-800",
|
|
},
|
|
"clean-minimal": {
|
|
"bg": "bg-white", "text": "text-gray-900",
|
|
"accent": "blue", "card_bg": "bg-gray-50 border border-gray-200 rounded-2xl",
|
|
"btn": "bg-blue-600 hover:bg-blue-700 text-white",
|
|
"btn_secondary": "border border-gray-300 text-gray-700 hover:bg-gray-50",
|
|
"section_alt": "bg-gray-50", "muted": "text-gray-500",
|
|
"border": "border-gray-200",
|
|
},
|
|
"bold-startup": {
|
|
"bg": "bg-white", "text": "text-gray-900",
|
|
"accent": "orange", "card_bg": "shadow-xl rounded-3xl bg-white",
|
|
"btn": "bg-orange-500 hover:bg-orange-600 text-white",
|
|
"btn_secondary": "border-2 border-orange-500 text-orange-600 hover:bg-orange-50",
|
|
"section_alt": "bg-orange-50/30", "muted": "text-gray-500",
|
|
"border": "border-gray-200",
|
|
},
|
|
"enterprise": {
|
|
"bg": "bg-slate-50", "text": "text-slate-900",
|
|
"accent": "slate", "card_bg": "bg-white border border-slate-200 shadow-sm",
|
|
"btn": "bg-slate-900 hover:bg-slate-800 text-white",
|
|
"btn_secondary": "border border-slate-300 text-slate-700 hover:bg-slate-100",
|
|
"section_alt": "bg-white", "muted": "text-slate-500",
|
|
"border": "border-slate-200",
|
|
},
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TSX generators
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def tsx_nav(config: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
brand = config.get("brand", "Brand")
|
|
nav_links = config.get("nav_links", [])
|
|
cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
|
|
links_jsx = "\n ".join(
|
|
f'<a href="{l.get("url", "#")}" className="{style["muted"]} hover:{style["text"]} font-medium transition-colors">{l.get("text", "")}</a>'
|
|
for l in nav_links
|
|
)
|
|
return f'''function Navbar() {{
|
|
return (
|
|
<nav className="sticky top-0 z-50 {style["bg"]} border-b {style["border"]} backdrop-blur-sm">
|
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
|
|
<a href="#" className="text-xl font-bold {style["text"]}">{brand}</a>
|
|
<div className="hidden items-center gap-8 md:flex">
|
|
{links_jsx}
|
|
<a href="{cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-5 py-2.5 text-sm font-semibold transition-colors">
|
|
{cta.get("text", "Get Started")}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def tsx_hero(hero: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
h1 = hero.get("headline", "Your Headline Here")
|
|
sub = hero.get("subheadline", "")
|
|
primary_cta = hero.get("primary_cta", {"text": "Get Started", "url": "#"})
|
|
secondary_cta = hero.get("secondary_cta", None)
|
|
secondary_jsx = ""
|
|
if secondary_cta:
|
|
secondary_jsx = f'''
|
|
<a href="{secondary_cta.get("url", "#")}" className="rounded-lg {style["btn_secondary"]} px-8 py-3 text-lg font-semibold transition-colors">
|
|
{secondary_cta.get("text", "Learn More")}
|
|
</a>'''
|
|
return f'''function Hero() {{
|
|
return (
|
|
<section className="flex min-h-[80vh] flex-col items-center justify-center px-6 py-24 text-center {style["bg"]}">
|
|
<div className="mx-auto max-w-4xl">
|
|
<h1 className="mb-6 text-5xl font-bold tracking-tight {style["text"]} md:text-7xl">
|
|
{h1}
|
|
</h1>
|
|
<p className="mx-auto mb-10 max-w-2xl text-xl {style["muted"]}">
|
|
{sub}
|
|
</p>
|
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
<a href="{primary_cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-8 py-3 text-lg font-semibold transition-colors">
|
|
{primary_cta.get("text", "Get Started")}
|
|
</a>{secondary_jsx}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def tsx_features(features: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
title = features.get("title", "Features")
|
|
subtitle = features.get("subtitle", "")
|
|
items = features.get("items", [])
|
|
cards_jsx = "\n ".join(
|
|
f'''<div className="{style["card_bg"]} rounded-xl p-8">
|
|
<div className="mb-4 text-3xl">{f.get("icon", "")}</div>
|
|
<h3 className="mb-3 text-xl font-semibold {style["text"]}">{f.get("title", "")}</h3>
|
|
<p className="{style["muted"]}">{f.get("description", "")}</p>
|
|
</div>'''
|
|
for f in items
|
|
)
|
|
return f'''function Features() {{
|
|
return (
|
|
<section className="{style["section_alt"]} px-6 py-24">
|
|
<div className="mx-auto max-w-7xl">
|
|
<h2 className="mb-4 text-center text-4xl font-bold {style["text"]}">{title}</h2>
|
|
<p className="mx-auto mb-16 max-w-2xl text-center text-lg {style["muted"]}">{subtitle}</p>
|
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
{cards_jsx}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def tsx_testimonials(testimonials: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
title = testimonials.get("title", "What Our Customers Say")
|
|
items = testimonials.get("items", [])
|
|
if not items:
|
|
return ""
|
|
cards_jsx = "\n ".join(
|
|
f'''<div className="rounded-xl border {style["border"]} p-8">
|
|
<p className="mb-6 text-lg italic {style["muted"]}">"{t.get("quote", "")}"</p>
|
|
<div>
|
|
<p className="font-semibold {style["text"]}">{t.get("name", "")}</p>
|
|
<p className="text-sm {style["muted"]}">{t.get("title", "")}, {t.get("company", "")}</p>
|
|
</div>
|
|
</div>'''
|
|
for t in items
|
|
)
|
|
return f'''function Testimonials() {{
|
|
return (
|
|
<section className="px-6 py-24 {style["bg"]}">
|
|
<div className="mx-auto max-w-7xl">
|
|
<h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
|
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
{cards_jsx}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def tsx_pricing(pricing: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
title = pricing.get("title", "Pricing")
|
|
plans = pricing.get("plans", [])
|
|
if not plans:
|
|
return ""
|
|
accent = style["accent"]
|
|
cards = []
|
|
for p in plans:
|
|
featured = p.get("featured", False)
|
|
border_cls = f"border-2 border-{accent}-500 ring-4 ring-{accent}-500/20" if featured else f"border {style['border']}"
|
|
badge = f'\n <div className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-{accent}-600 px-4 py-1 text-xs font-semibold text-white">Most Popular</div>' if featured else ""
|
|
features_jsx = "\n ".join(
|
|
f'<li className="flex items-center gap-2 py-2"><span className="text-{accent}-500 font-bold">✓</span> {feat}</li>'
|
|
for feat in p.get("features", [])
|
|
)
|
|
cards.append(f'''<div className="relative rounded-2xl {border_cls} {style["card_bg"]} p-8 text-center">{badge}
|
|
<h3 className="mb-2 text-xl font-semibold {style["text"]}">{p.get("name", "")}</h3>
|
|
<div className="my-6 text-5xl font-extrabold {style["text"]}">${p.get("price", "0")}<span className="text-base font-normal {style["muted"]}">/mo</span></div>
|
|
<p className="{style["muted"]} mb-6">{p.get("description", "")}</p>
|
|
<ul className="mb-8 space-y-1 text-left {style["muted"]}">
|
|
{features_jsx}
|
|
</ul>
|
|
<a href="{p.get("cta_url", "#")}" className="block w-full rounded-lg {style["btn"]} py-3 text-center font-semibold transition-colors">
|
|
{p.get("cta_text", "Choose Plan")}
|
|
</a>
|
|
</div>''')
|
|
cards_jsx = "\n ".join(cards)
|
|
return f'''function Pricing() {{
|
|
return (
|
|
<section className="{style["section_alt"]} px-6 py-24">
|
|
<div className="mx-auto max-w-5xl">
|
|
<h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
|
|
<div className="grid gap-8 lg:grid-cols-{min(len(plans), 3)}">
|
|
{cards_jsx}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def tsx_cta(cta: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
accent = style["accent"]
|
|
return f'''function CTASection() {{
|
|
return (
|
|
<section className="bg-{accent}-600 px-6 py-24 text-center text-white">
|
|
<div className="mx-auto max-w-3xl">
|
|
<h2 className="mb-4 text-4xl font-bold">{cta.get("headline", "Ready to get started?")}</h2>
|
|
<p className="mb-10 text-xl opacity-90">{cta.get("subheadline", "")}</p>
|
|
<a href="{cta.get("url", "#")}" className="rounded-lg bg-white px-8 py-3 text-lg font-semibold text-{accent}-600 transition-colors hover:bg-gray-100">
|
|
{cta.get("text", "Start Free Trial")}
|
|
</a>
|
|
</div>
|
|
</section>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def tsx_footer(config: Dict[str, Any], style: Dict[str, str]) -> str:
|
|
brand = config.get("brand", "Company")
|
|
year = datetime.now().year
|
|
footer_text = config.get("footer_text", f"{year} {brand}. All rights reserved.")
|
|
return f'''function Footer() {{
|
|
return (
|
|
<footer className="border-t {style["border"]} {style["bg"]} px-6 py-10 text-center {style["muted"]}">
|
|
<p>© {footer_text}</p>
|
|
</footer>
|
|
);
|
|
}}'''
|
|
|
|
|
|
def generate_tsx(config: Dict[str, Any]) -> str:
|
|
"""Generate complete Next.js/React TSX landing page with Tailwind CSS."""
|
|
style_name = config.get("design_style", "clean-minimal")
|
|
style = DESIGN_STYLES.get(style_name, DESIGN_STYLES["clean-minimal"])
|
|
|
|
components = []
|
|
component_names = []
|
|
|
|
components.append(tsx_nav(config, style))
|
|
component_names.append("Navbar")
|
|
|
|
if config.get("hero"):
|
|
components.append(tsx_hero(config["hero"], style))
|
|
component_names.append("Hero")
|
|
|
|
if config.get("features"):
|
|
components.append(tsx_features(config["features"], style))
|
|
component_names.append("Features")
|
|
|
|
if config.get("testimonials") and config["testimonials"].get("items"):
|
|
components.append(tsx_testimonials(config["testimonials"], style))
|
|
component_names.append("Testimonials")
|
|
|
|
if config.get("pricing") and config["pricing"].get("plans"):
|
|
components.append(tsx_pricing(config["pricing"], style))
|
|
component_names.append("Pricing")
|
|
|
|
if config.get("cta"):
|
|
components.append(tsx_cta(config["cta"], style))
|
|
component_names.append("CTASection")
|
|
|
|
components.append(tsx_footer(config, style))
|
|
component_names.append("Footer")
|
|
|
|
title = config.get("title", "Landing Page")
|
|
meta_desc = config.get("meta_description", "")
|
|
|
|
page_body = "\n ".join(f"<{name} />" for name in component_names)
|
|
all_components = "\n\n".join(components)
|
|
|
|
return f'''// Generated by Landing Page Scaffolder — {datetime.now().strftime("%Y-%m-%d")}
|
|
// Stack: Next.js 14+ App Router, React, Tailwind CSS
|
|
// Design style: {style_name}
|
|
|
|
import type {{ Metadata }} from "next";
|
|
|
|
export const metadata: Metadata = {{
|
|
title: "{title}",
|
|
description: "{meta_desc}",
|
|
openGraph: {{
|
|
title: "{title}",
|
|
description: "{meta_desc}",
|
|
type: "website",
|
|
}},
|
|
}};
|
|
|
|
{all_components}
|
|
|
|
export default function LandingPage() {{
|
|
return (
|
|
<main>
|
|
{page_body}
|
|
</main>
|
|
);
|
|
}}
|
|
'''
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HTML generators (existing)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def generate_css(config: Dict[str, Any]) -> str:
|
|
"""Generate responsive CSS from config theme."""
|
|
theme = config.get("theme", {})
|
|
primary = theme.get("primary_color", "#2563eb")
|
|
secondary = theme.get("secondary_color", "#1e40af")
|
|
bg = theme.get("background", "#ffffff")
|
|
text_color = theme.get("text_color", "#1f2937")
|
|
font = theme.get("font", "Inter, system-ui, -apple-system, sans-serif")
|
|
|
|
return f"""
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{ font-family: {font}; color: {text_color}; background: {bg}; line-height: 1.6; }}
|
|
.container {{ max-width: 1200px; margin: 0 auto; padding: 0 24px; }}
|
|
nav {{ padding: 16px 0; border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; background: {bg}; z-index: 100; }}
|
|
nav .container {{ display: flex; justify-content: space-between; align-items: center; }}
|
|
.nav-logo {{ font-size: 1.5rem; font-weight: 700; color: {primary}; text-decoration: none; }}
|
|
.nav-links {{ display: flex; gap: 24px; list-style: none; }}
|
|
.nav-links a {{ text-decoration: none; color: {text_color}; font-weight: 500; }}
|
|
.nav-cta {{ background: {primary}; color: white; padding: 8px 20px; border-radius: 6px; text-decoration: none; font-weight: 600; }}
|
|
.hero {{ padding: 80px 0; text-align: center; }}
|
|
.hero h1 {{ font-size: 3.5rem; font-weight: 800; line-height: 1.1; margin-bottom: 24px; max-width: 800px; margin-left: auto; margin-right: auto; }}
|
|
.hero p {{ font-size: 1.25rem; color: #6b7280; max-width: 600px; margin: 0 auto 32px; }}
|
|
.hero-cta {{ display: inline-flex; gap: 16px; }}
|
|
.btn-primary {{ background: {primary}; color: white; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; }}
|
|
.btn-secondary {{ background: transparent; color: {primary}; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; border: 2px solid {primary}; }}
|
|
.features {{ padding: 80px 0; background: #f9fafb; }}
|
|
.section-title {{ text-align: center; font-size: 2.5rem; font-weight: 700; margin-bottom: 16px; }}
|
|
.section-subtitle {{ text-align: center; color: #6b7280; font-size: 1.1rem; margin-bottom: 48px; max-width: 600px; margin-left: auto; margin-right: auto; }}
|
|
.features-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; }}
|
|
.feature-card {{ background: white; padding: 32px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
|
|
.feature-icon {{ font-size: 2rem; margin-bottom: 16px; }}
|
|
.feature-card h3 {{ font-size: 1.25rem; margin-bottom: 12px; }}
|
|
.feature-card p {{ color: #6b7280; }}
|
|
.testimonials {{ padding: 80px 0; }}
|
|
.testimonials-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 24px; }}
|
|
.testimonial-card {{ padding: 32px; border: 1px solid #e5e7eb; border-radius: 12px; }}
|
|
.testimonial-text {{ font-size: 1.1rem; font-style: italic; margin-bottom: 20px; }}
|
|
.testimonial-author {{ display: flex; align-items: center; gap: 12px; }}
|
|
.author-info strong {{ display: block; }}
|
|
.author-info span {{ color: #6b7280; font-size: 0.9rem; }}
|
|
.pricing {{ padding: 80px 0; background: #f9fafb; }}
|
|
.pricing-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; max-width: 900px; margin: 0 auto; }}
|
|
.pricing-card {{ background: white; padding: 32px; border-radius: 12px; border: 2px solid #e5e7eb; text-align: center; }}
|
|
.pricing-card.featured {{ border-color: {primary}; position: relative; }}
|
|
.pricing-card.featured::before {{ content: "Most Popular"; position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: {primary}; color: white; padding: 4px 16px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; }}
|
|
.pricing-name {{ font-size: 1.25rem; font-weight: 600; margin-bottom: 8px; }}
|
|
.pricing-price {{ font-size: 3rem; font-weight: 800; margin: 16px 0; }}
|
|
.pricing-price span {{ font-size: 1rem; font-weight: 400; color: #6b7280; }}
|
|
.pricing-features {{ list-style: none; text-align: left; margin: 24px 0; }}
|
|
.pricing-features li {{ padding: 8px 0; border-bottom: 1px solid #f3f4f6; }}
|
|
.pricing-features li::before {{ content: "\\2713 "; color: {primary}; font-weight: 700; }}
|
|
.cta-section {{ padding: 80px 0; text-align: center; background: {primary}; color: white; }}
|
|
.cta-section h2 {{ font-size: 2.5rem; margin-bottom: 16px; }}
|
|
.cta-section p {{ font-size: 1.1rem; opacity: 0.9; margin-bottom: 32px; }}
|
|
.btn-white {{ background: white; color: {primary}; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; }}
|
|
footer {{ padding: 40px 0; border-top: 1px solid #e5e7eb; color: #6b7280; text-align: center; }}
|
|
@media (max-width: 768px) {{
|
|
.hero h1 {{ font-size: 2.25rem; }}
|
|
.hero-cta {{ flex-direction: column; align-items: center; }}
|
|
.nav-links {{ display: none; }}
|
|
.features-grid {{ grid-template-columns: 1fr; }}
|
|
.pricing-grid {{ grid-template-columns: 1fr; }}
|
|
}}
|
|
"""
|
|
|
|
|
|
def render_nav(config: Dict[str, Any]) -> str:
|
|
brand = escape(config.get("brand", "Brand"))
|
|
nav_links = config.get("nav_links", [])
|
|
cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
|
|
links = "\n".join(
|
|
f'<li><a href="{escape(l.get("url", "#"))}">{escape(l.get("text", ""))}</a></li>'
|
|
for l in nav_links
|
|
)
|
|
return f"""
|
|
<nav><div class="container">
|
|
<a href="#" class="nav-logo">{brand}</a>
|
|
<ul class="nav-links">{links}</ul>
|
|
<a href="{escape(cta.get('url', '#'))}" class="nav-cta">{escape(cta.get('text', 'Get Started'))}</a>
|
|
</div></nav>"""
|
|
|
|
|
|
def render_hero(hero: Dict[str, Any]) -> str:
|
|
h1 = escape(hero.get("headline", "Your Headline Here"))
|
|
sub = escape(hero.get("subheadline", ""))
|
|
primary_cta = hero.get("primary_cta", {"text": "Get Started", "url": "#"})
|
|
secondary_cta = hero.get("secondary_cta", None)
|
|
cta_html = f'<a href="{escape(primary_cta.get("url", "#"))}" class="btn-primary">{escape(primary_cta.get("text", "Get Started"))}</a>'
|
|
if secondary_cta:
|
|
cta_html += f'\n<a href="{escape(secondary_cta.get("url", "#"))}" class="btn-secondary">{escape(secondary_cta.get("text", "Learn More"))}</a>'
|
|
return f"""
|
|
<section class="hero"><div class="container">
|
|
<h1>{h1}</h1>
|
|
<p>{sub}</p>
|
|
<div class="hero-cta">{cta_html}</div>
|
|
</div></section>"""
|
|
|
|
|
|
def render_features(features: Dict[str, Any]) -> str:
|
|
title = escape(features.get("title", "Features"))
|
|
subtitle = escape(features.get("subtitle", ""))
|
|
items = features.get("items", [])
|
|
cards = "\n".join(f"""
|
|
<div class="feature-card">
|
|
<div class="feature-icon">{escape(f.get('icon', ''))}</div>
|
|
<h3>{escape(f.get('title', ''))}</h3>
|
|
<p>{escape(f.get('description', ''))}</p>
|
|
</div>""" for f in items)
|
|
return f"""
|
|
<section class="features"><div class="container">
|
|
<h2 class="section-title">{title}</h2>
|
|
<p class="section-subtitle">{subtitle}</p>
|
|
<div class="features-grid">{cards}</div>
|
|
</div></section>"""
|
|
|
|
|
|
def render_testimonials(testimonials: Dict[str, Any]) -> str:
|
|
title = escape(testimonials.get("title", "What Our Customers Say"))
|
|
items = testimonials.get("items", [])
|
|
if not items:
|
|
return ""
|
|
cards = "\n".join(f"""
|
|
<div class="testimonial-card">
|
|
<p class="testimonial-text">"{escape(t.get('quote', ''))}"</p>
|
|
<div class="testimonial-author">
|
|
<div class="author-info">
|
|
<strong>{escape(t.get('name', ''))}</strong>
|
|
<span>{escape(t.get('title', ''))}, {escape(t.get('company', ''))}</span>
|
|
</div>
|
|
</div>
|
|
</div>""" for t in items)
|
|
return f"""
|
|
<section class="testimonials"><div class="container">
|
|
<h2 class="section-title">{title}</h2>
|
|
<div class="testimonials-grid">{cards}</div>
|
|
</div></section>"""
|
|
|
|
|
|
def render_pricing(pricing: Dict[str, Any]) -> str:
|
|
title = escape(pricing.get("title", "Pricing"))
|
|
plans = pricing.get("plans", [])
|
|
if not plans:
|
|
return ""
|
|
cards = "\n".join(f"""
|
|
<div class="pricing-card {'featured' if p.get('featured') else ''}">
|
|
<div class="pricing-name">{escape(p.get('name', ''))}</div>
|
|
<div class="pricing-price">${escape(str(p.get('price', '0')))}<span>/mo</span></div>
|
|
<p>{escape(p.get('description', ''))}</p>
|
|
<ul class="pricing-features">
|
|
{"".join(f'<li>{escape(f)}</li>' for f in p.get('features', []))}
|
|
</ul>
|
|
<a href="{escape(p.get('cta_url', '#'))}" class="btn-primary">{escape(p.get('cta_text', 'Choose Plan'))}</a>
|
|
</div>""" for p in plans)
|
|
return f"""
|
|
<section class="pricing"><div class="container">
|
|
<h2 class="section-title">{title}</h2>
|
|
<div class="pricing-grid">{cards}</div>
|
|
</div></section>"""
|
|
|
|
|
|
def render_cta(cta: Dict[str, Any]) -> str:
|
|
return f"""
|
|
<section class="cta-section"><div class="container">
|
|
<h2>{escape(cta.get('headline', 'Ready to get started?'))}</h2>
|
|
<p>{escape(cta.get('subheadline', ''))}</p>
|
|
<a href="{escape(cta.get('url', '#'))}" class="btn-white">{escape(cta.get('text', 'Start Free Trial'))}</a>
|
|
</div></section>"""
|
|
|
|
|
|
def generate_html(config: Dict[str, Any]) -> str:
|
|
"""Generate complete HTML landing page."""
|
|
title = escape(config.get("title", "Landing Page"))
|
|
css = generate_css(config)
|
|
sections = []
|
|
sections.append(render_nav(config))
|
|
if config.get("hero"):
|
|
sections.append(render_hero(config["hero"]))
|
|
if config.get("features"):
|
|
sections.append(render_features(config["features"]))
|
|
if config.get("testimonials"):
|
|
sections.append(render_testimonials(config["testimonials"]))
|
|
if config.get("pricing"):
|
|
sections.append(render_pricing(config["pricing"]))
|
|
if config.get("cta"):
|
|
sections.append(render_cta(config["cta"]))
|
|
sections.append(f"""
|
|
<footer><div class="container">
|
|
<p>{escape(config.get('footer_text', f'{datetime.now().year} {config.get("brand", "Company")}. All rights reserved.'))}</p>
|
|
</div></footer>""")
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{title}</title>
|
|
<meta name="description" content="{escape(config.get('meta_description', ''))}">
|
|
<style>{css}</style>
|
|
</head>
|
|
<body>
|
|
{"".join(sections)}
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate landing pages as HTML or Next.js TSX with Tailwind CSS"
|
|
)
|
|
parser.add_argument("input", help="Path to page config JSON")
|
|
parser.add_argument(
|
|
"--format", choices=["html", "tsx", "json"], default="tsx",
|
|
help="Output format: tsx (Next.js + Tailwind), html (standalone), json (metadata)"
|
|
)
|
|
parser.add_argument("--output", type=str, default=None, help="Output file path")
|
|
|
|
args = parser.parse_args()
|
|
|
|
with open(args.input) as f:
|
|
config = json.load(f)
|
|
|
|
if args.format == "json":
|
|
output = json.dumps({
|
|
"generated_at": datetime.now().isoformat(),
|
|
"config": config,
|
|
"formats_available": ["html", "tsx"],
|
|
"sections": [k for k in ["nav", "hero", "features", "testimonials", "pricing", "cta", "footer"]
|
|
if config.get(k) or k in ("nav", "footer")]
|
|
}, indent=2)
|
|
elif args.format == "tsx":
|
|
output = generate_tsx(config)
|
|
else:
|
|
output = generate_html(config)
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
f.write(output)
|
|
print(f"Landing page written to {args.output}")
|
|
else:
|
|
print(output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|