opencode-skills-antigravity 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +5 -3
  2. package/bundled-skills/007/scripts/full_audit.py +6 -4
  3. package/bundled-skills/007/scripts/score_calculator.py +67 -7
  4. package/bundled-skills/algorithmic-art/templates/viewer.html +2 -2
  5. package/bundled-skills/apify-actorization/SKILL.md +1 -2
  6. package/bundled-skills/apify-actorization/references/cli-actorization.md +4 -4
  7. package/bundled-skills/docs/COMMUNITY_GUIDELINES.md +1 -1
  8. package/bundled-skills/docs/contributors/community-guidelines.md +3 -32
  9. package/bundled-skills/docs/integrations/jetski-gemini-loader/loader.ts +21 -3
  10. package/bundled-skills/docs/maintainers/security-findings-triage-2026-03-18-addendum.md +22 -0
  11. package/bundled-skills/docs/users/getting-started.md +1 -1
  12. package/bundled-skills/docs/users/walkthrough.md +21 -17
  13. package/bundled-skills/dotnet-backend-patterns/resources/implementation-playbook.md +2 -2
  14. package/bundled-skills/instagram/scripts/auth.py +15 -6
  15. package/bundled-skills/landing-page-generator/SKILL.md +203 -0
  16. package/bundled-skills/landing-page-generator/references/conversion-patterns.md +176 -0
  17. package/bundled-skills/landing-page-generator/references/frameworks.md +177 -0
  18. package/bundled-skills/landing-page-generator/references/landing-page-patterns.md +98 -0
  19. package/bundled-skills/landing-page-generator/references/seo-checklist.md +109 -0
  20. package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +568 -0
  21. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +33 -1073
  22. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +7 -4
  23. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +15 -3
  24. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +85 -88
  25. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +260 -456
  26. package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +4 -2
  27. package/bundled-skills/notebooklm/scripts/auth_manager.py +17 -3
  28. package/bundled-skills/notebooklm/scripts/browser_session.py +11 -2
  29. package/bundled-skills/radix-ui-design-system/examples/README.md +1 -1
  30. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +5 -3
  31. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/app.py +21 -13
  32. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/webhook_handler.py +11 -4
  33. package/package.json +1 -1
  34. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +0 -35
  35. /package/bundled-skills/dotnet-backend-patterns/assets/{repository-template.cs → repository-template.cs.template} +0 -0
  36. /package/bundled-skills/dotnet-backend-patterns/assets/{service-template.cs → service-template.cs.template} +0 -0
  37. /package/bundled-skills/radix-ui-design-system/templates/{component-template.tsx → component-template.tsx.template} +0 -0
@@ -0,0 +1,109 @@
1
+ # Landing Page SEO Checklist
2
+
3
+ ## Overview
4
+
5
+ This checklist ensures landing pages are optimized for search engine visibility while maintaining conversion focus. Apply these checks before launching any landing page.
6
+
7
+ ## Meta Tags
8
+
9
+ - [ ] **Title tag**: Under 60 characters, includes primary keyword, ends with brand name
10
+ - [ ] **Meta description**: 150-160 characters, includes CTA language, unique per page
11
+ - [ ] **Canonical URL**: Set to prevent duplicate content issues
12
+ - [ ] **Robots meta**: Ensure page is indexable (`index, follow`) unless intentionally noindex
13
+ - [ ] **Open Graph tags**: og:title, og:description, og:image, og:url for social sharing
14
+ - [ ] **Twitter Card tags**: twitter:card, twitter:title, twitter:description, twitter:image
15
+ - [ ] **Viewport meta**: `<meta name="viewport" content="width=device-width, initial-scale=1">`
16
+
17
+ ## Structured Data
18
+
19
+ - [ ] **Organization schema**: Company name, logo, social profiles
20
+ - [ ] **Product schema**: Name, description, price, availability (for product pages)
21
+ - [ ] **FAQ schema**: For pages with FAQ sections (rich snippet opportunity)
22
+ - [ ] **Breadcrumb schema**: Navigation path for deep pages
23
+ - [ ] **Review schema**: Aggregate rating if testimonials present (use carefully per guidelines)
24
+ - [ ] **Validate**: Test all structured data with Google Rich Results Test
25
+
26
+ ## Core Web Vitals Targets
27
+
28
+ ### Largest Contentful Paint (LCP) - Target: < 2.5s
29
+ - [ ] Optimize hero image (WebP format, proper dimensions)
30
+ - [ ] Preload critical resources (`<link rel="preload">`)
31
+ - [ ] Use CDN for static assets
32
+ - [ ] Minimize render-blocking CSS and JavaScript
33
+
34
+ ### First Input Delay (FID) / Interaction to Next Paint (INP) - Target: < 200ms
35
+ - [ ] Defer non-critical JavaScript
36
+ - [ ] Break up long tasks (>50ms)
37
+ - [ ] Minimize third-party script impact
38
+ - [ ] Use `requestAnimationFrame` for visual updates
39
+
40
+ ### Cumulative Layout Shift (CLS) - Target: < 0.1
41
+ - [ ] Set explicit width/height on images and videos
42
+ - [ ] Reserve space for dynamic content (ads, embeds)
43
+ - [ ] Use `font-display: swap` for web fonts
44
+ - [ ] Avoid inserting content above existing content
45
+
46
+ ## Keyword Placement
47
+
48
+ - [ ] **H1 tag**: Contains primary keyword, one per page only
49
+ - [ ] **H2 tags**: Include secondary keywords naturally
50
+ - [ ] **First paragraph**: Primary keyword appears in first 100 words
51
+ - [ ] **Body copy**: Natural keyword density (1-2%), no stuffing
52
+ - [ ] **Image alt text**: Descriptive, includes keyword where relevant
53
+ - [ ] **URL slug**: Short, keyword-rich, hyphen-separated
54
+ - [ ] **CTA text**: Consider keyword inclusion where natural
55
+
56
+ ## Internal Linking
57
+
58
+ - [ ] Link to relevant product/feature pages
59
+ - [ ] Link to blog content that supports the page topic
60
+ - [ ] Use descriptive anchor text (not "click here")
61
+ - [ ] Ensure landing page is linked from main navigation or sitemap
62
+ - [ ] Link to pricing page if applicable
63
+ - [ ] Limit links to avoid diluting page authority (15-20 max)
64
+
65
+ ## Image Optimization
66
+
67
+ - [ ] **Format**: Use WebP with JPEG/PNG fallback
68
+ - [ ] **Compression**: Lossy compression for photos, lossless for graphics
69
+ - [ ] **Dimensions**: Serve at exact display size (no CSS resizing)
70
+ - [ ] **Alt text**: Descriptive, 125 characters max, natural keyword inclusion
71
+ - [ ] **File names**: Descriptive, hyphenated (e.g., `product-dashboard-screenshot.webp`)
72
+ - [ ] **Lazy loading**: Apply to images below the fold (`loading="lazy"`)
73
+ - [ ] **Responsive images**: Use `srcset` for different viewport sizes
74
+
75
+ ## Canonical URLs
76
+
77
+ - [ ] Self-referencing canonical on every page
78
+ - [ ] Consistent protocol (https) and trailing slash usage
79
+ - [ ] Canonical points to preferred URL version (www vs non-www)
80
+ - [ ] UTM parameters excluded from canonical URL
81
+ - [ ] Pagination handled with rel="next"/"prev" or single-page canonical
82
+
83
+ ## Mobile Responsiveness
84
+
85
+ - [ ] **Mobile-friendly test**: Pass Google Mobile-Friendly Test
86
+ - [ ] **Touch targets**: Minimum 44x44px, 8px spacing between targets
87
+ - [ ] **Font size**: Minimum 16px base font, no pinch-to-zoom needed
88
+ - [ ] **Content parity**: All critical content accessible on mobile
89
+ - [ ] **Horizontal scroll**: None present at any viewport width
90
+ - [ ] **Form usability**: Appropriate input types (email, tel), autocomplete attributes
91
+ - [ ] **Media queries**: Breakpoints at 480px, 768px, 1024px, 1200px minimum
92
+
93
+ ## Technical SEO
94
+
95
+ - [ ] **HTTPS**: SSL certificate valid and active
96
+ - [ ] **Page speed**: < 3s load time on mobile (test with PageSpeed Insights)
97
+ - [ ] **XML sitemap**: Page included in sitemap.xml
98
+ - [ ] **Robots.txt**: Page not blocked by robots.txt
99
+ - [ ] **404 handling**: Custom 404 page with navigation
100
+ - [ ] **Redirect chains**: No more than 1 redirect hop
101
+ - [ ] **Hreflang**: Set for multi-language landing pages
102
+
103
+ ## Content Quality Signals
104
+
105
+ - [ ] **Unique content**: No duplicate content from other pages
106
+ - [ ] **Content depth**: Sufficient content for topic coverage (500+ words for SEO pages)
107
+ - [ ] **Readability**: Grade level 6-8 for broad audiences
108
+ - [ ] **Freshness**: Last modified date reflects recent updates
109
+ - [ ] **E-E-A-T signals**: Author expertise, company authority, trust indicators
@@ -0,0 +1,568 @@
1
+ #!/usr/bin/env python3
2
+ """Landing Page Scaffolder — Generate landing pages as HTML or Next.js TSX from config.
3
+
4
+ Creates production-ready landing pages with hero sections, features,
5
+ testimonials, pricing, CTAs, and responsive design.
6
+
7
+ Usage:
8
+ python landing_page_scaffolder.py config.json --format html --output page.html
9
+ python landing_page_scaffolder.py config.json --format tsx --output LandingPage.tsx
10
+ python landing_page_scaffolder.py config.json --format json
11
+ """
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from typing import Dict, List, Any, Optional
17
+ from datetime import datetime
18
+ import html as html_module
19
+
20
+
21
+ def escape(text: str) -> str:
22
+ """HTML-escape text."""
23
+ return html_module.escape(str(text))
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Tailwind style mappings for TSX output
28
+ # ---------------------------------------------------------------------------
29
+
30
+ DESIGN_STYLES = {
31
+ "dark-saas": {
32
+ "bg": "bg-gray-950", "text": "text-white",
33
+ "accent": "violet", "card_bg": "bg-gray-900 border border-gray-800",
34
+ "btn": "bg-violet-600 hover:bg-violet-500 text-white",
35
+ "btn_secondary": "border border-gray-700 text-gray-300 hover:bg-gray-800",
36
+ "section_alt": "bg-gray-900/50", "muted": "text-gray-400",
37
+ "border": "border-gray-800",
38
+ },
39
+ "clean-minimal": {
40
+ "bg": "bg-white", "text": "text-gray-900",
41
+ "accent": "blue", "card_bg": "bg-gray-50 border border-gray-200 rounded-2xl",
42
+ "btn": "bg-blue-600 hover:bg-blue-700 text-white",
43
+ "btn_secondary": "border border-gray-300 text-gray-700 hover:bg-gray-50",
44
+ "section_alt": "bg-gray-50", "muted": "text-gray-500",
45
+ "border": "border-gray-200",
46
+ },
47
+ "bold-startup": {
48
+ "bg": "bg-white", "text": "text-gray-900",
49
+ "accent": "orange", "card_bg": "shadow-xl rounded-3xl bg-white",
50
+ "btn": "bg-orange-500 hover:bg-orange-600 text-white",
51
+ "btn_secondary": "border-2 border-orange-500 text-orange-600 hover:bg-orange-50",
52
+ "section_alt": "bg-orange-50/30", "muted": "text-gray-500",
53
+ "border": "border-gray-200",
54
+ },
55
+ "enterprise": {
56
+ "bg": "bg-slate-50", "text": "text-slate-900",
57
+ "accent": "slate", "card_bg": "bg-white border border-slate-200 shadow-sm",
58
+ "btn": "bg-slate-900 hover:bg-slate-800 text-white",
59
+ "btn_secondary": "border border-slate-300 text-slate-700 hover:bg-slate-100",
60
+ "section_alt": "bg-white", "muted": "text-slate-500",
61
+ "border": "border-slate-200",
62
+ },
63
+ }
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # TSX generators
68
+ # ---------------------------------------------------------------------------
69
+
70
+ def tsx_nav(config: Dict[str, Any], style: Dict[str, str]) -> str:
71
+ brand = config.get("brand", "Brand")
72
+ nav_links = config.get("nav_links", [])
73
+ cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
74
+ links_jsx = "\n ".join(
75
+ f'<a href="{l.get("url", "#")}" className="{style["muted"]} hover:{style["text"]} font-medium transition-colors">{l.get("text", "")}</a>'
76
+ for l in nav_links
77
+ )
78
+ return f'''function Navbar() {{
79
+ return (
80
+ <nav className="sticky top-0 z-50 {style["bg"]} border-b {style["border"]} backdrop-blur-sm">
81
+ <div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
82
+ <a href="#" className="text-xl font-bold {style["text"]}">{brand}</a>
83
+ <div className="hidden items-center gap-8 md:flex">
84
+ {links_jsx}
85
+ <a href="{cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-5 py-2.5 text-sm font-semibold transition-colors">
86
+ {cta.get("text", "Get Started")}
87
+ </a>
88
+ </div>
89
+ </div>
90
+ </nav>
91
+ );
92
+ }}'''
93
+
94
+
95
+ def tsx_hero(hero: Dict[str, Any], style: Dict[str, str]) -> str:
96
+ h1 = hero.get("headline", "Your Headline Here")
97
+ sub = hero.get("subheadline", "")
98
+ primary_cta = hero.get("primary_cta", {"text": "Get Started", "url": "#"})
99
+ secondary_cta = hero.get("secondary_cta", None)
100
+ secondary_jsx = ""
101
+ if secondary_cta:
102
+ secondary_jsx = f'''
103
+ <a href="{secondary_cta.get("url", "#")}" className="rounded-lg {style["btn_secondary"]} px-8 py-3 text-lg font-semibold transition-colors">
104
+ {secondary_cta.get("text", "Learn More")}
105
+ </a>'''
106
+ return f'''function Hero() {{
107
+ return (
108
+ <section className="flex min-h-[80vh] flex-col items-center justify-center px-6 py-24 text-center {style["bg"]}">
109
+ <div className="mx-auto max-w-4xl">
110
+ <h1 className="mb-6 text-5xl font-bold tracking-tight {style["text"]} md:text-7xl">
111
+ {h1}
112
+ </h1>
113
+ <p className="mx-auto mb-10 max-w-2xl text-xl {style["muted"]}">
114
+ {sub}
115
+ </p>
116
+ <div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
117
+ <a href="{primary_cta.get("url", "#")}" className="rounded-lg {style["btn"]} px-8 py-3 text-lg font-semibold transition-colors">
118
+ {primary_cta.get("text", "Get Started")}
119
+ </a>{secondary_jsx}
120
+ </div>
121
+ </div>
122
+ </section>
123
+ );
124
+ }}'''
125
+
126
+
127
+ def tsx_features(features: Dict[str, Any], style: Dict[str, str]) -> str:
128
+ title = features.get("title", "Features")
129
+ subtitle = features.get("subtitle", "")
130
+ items = features.get("items", [])
131
+ cards_jsx = "\n ".join(
132
+ f'''<div className="{style["card_bg"]} rounded-xl p-8">
133
+ <div className="mb-4 text-3xl">{f.get("icon", "")}</div>
134
+ <h3 className="mb-3 text-xl font-semibold {style["text"]}">{f.get("title", "")}</h3>
135
+ <p className="{style["muted"]}">{f.get("description", "")}</p>
136
+ </div>'''
137
+ for f in items
138
+ )
139
+ return f'''function Features() {{
140
+ return (
141
+ <section className="{style["section_alt"]} px-6 py-24">
142
+ <div className="mx-auto max-w-7xl">
143
+ <h2 className="mb-4 text-center text-4xl font-bold {style["text"]}">{title}</h2>
144
+ <p className="mx-auto mb-16 max-w-2xl text-center text-lg {style["muted"]}">{subtitle}</p>
145
+ <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
146
+ {cards_jsx}
147
+ </div>
148
+ </div>
149
+ </section>
150
+ );
151
+ }}'''
152
+
153
+
154
+ def tsx_testimonials(testimonials: Dict[str, Any], style: Dict[str, str]) -> str:
155
+ title = testimonials.get("title", "What Our Customers Say")
156
+ items = testimonials.get("items", [])
157
+ if not items:
158
+ return ""
159
+ cards_jsx = "\n ".join(
160
+ f'''<div className="rounded-xl border {style["border"]} p-8">
161
+ <p className="mb-6 text-lg italic {style["muted"]}">"{t.get("quote", "")}"</p>
162
+ <div>
163
+ <p className="font-semibold {style["text"]}">{t.get("name", "")}</p>
164
+ <p className="text-sm {style["muted"]}">{t.get("title", "")}, {t.get("company", "")}</p>
165
+ </div>
166
+ </div>'''
167
+ for t in items
168
+ )
169
+ return f'''function Testimonials() {{
170
+ return (
171
+ <section className="px-6 py-24 {style["bg"]}">
172
+ <div className="mx-auto max-w-7xl">
173
+ <h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
174
+ <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
175
+ {cards_jsx}
176
+ </div>
177
+ </div>
178
+ </section>
179
+ );
180
+ }}'''
181
+
182
+
183
+ def tsx_pricing(pricing: Dict[str, Any], style: Dict[str, str]) -> str:
184
+ title = pricing.get("title", "Pricing")
185
+ plans = pricing.get("plans", [])
186
+ if not plans:
187
+ return ""
188
+ accent = style["accent"]
189
+ cards = []
190
+ for p in plans:
191
+ featured = p.get("featured", False)
192
+ border_cls = f"border-2 border-{accent}-500 ring-4 ring-{accent}-500/20" if featured else f"border {style['border']}"
193
+ 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 ""
194
+ features_jsx = "\n ".join(
195
+ f'<li className="flex items-center gap-2 py-2"><span className="text-{accent}-500 font-bold">&#10003;</span> {feat}</li>'
196
+ for feat in p.get("features", [])
197
+ )
198
+ cards.append(f'''<div className="relative rounded-2xl {border_cls} {style["card_bg"]} p-8 text-center">{badge}
199
+ <h3 className="mb-2 text-xl font-semibold {style["text"]}">{p.get("name", "")}</h3>
200
+ <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>
201
+ <p className="{style["muted"]} mb-6">{p.get("description", "")}</p>
202
+ <ul className="mb-8 space-y-1 text-left {style["muted"]}">
203
+ {features_jsx}
204
+ </ul>
205
+ <a href="{p.get("cta_url", "#")}" className="block w-full rounded-lg {style["btn"]} py-3 text-center font-semibold transition-colors">
206
+ {p.get("cta_text", "Choose Plan")}
207
+ </a>
208
+ </div>''')
209
+ cards_jsx = "\n ".join(cards)
210
+ return f'''function Pricing() {{
211
+ return (
212
+ <section className="{style["section_alt"]} px-6 py-24">
213
+ <div className="mx-auto max-w-5xl">
214
+ <h2 className="mb-16 text-center text-4xl font-bold {style["text"]}">{title}</h2>
215
+ <div className="grid gap-8 lg:grid-cols-{min(len(plans), 3)}">
216
+ {cards_jsx}
217
+ </div>
218
+ </div>
219
+ </section>
220
+ );
221
+ }}'''
222
+
223
+
224
+ def tsx_cta(cta: Dict[str, Any], style: Dict[str, str]) -> str:
225
+ accent = style["accent"]
226
+ return f'''function CTASection() {{
227
+ return (
228
+ <section className="bg-{accent}-600 px-6 py-24 text-center text-white">
229
+ <div className="mx-auto max-w-3xl">
230
+ <h2 className="mb-4 text-4xl font-bold">{cta.get("headline", "Ready to get started?")}</h2>
231
+ <p className="mb-10 text-xl opacity-90">{cta.get("subheadline", "")}</p>
232
+ <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">
233
+ {cta.get("text", "Start Free Trial")}
234
+ </a>
235
+ </div>
236
+ </section>
237
+ );
238
+ }}'''
239
+
240
+
241
+ def tsx_footer(config: Dict[str, Any], style: Dict[str, str]) -> str:
242
+ brand = config.get("brand", "Company")
243
+ year = datetime.now().year
244
+ footer_text = config.get("footer_text", f"{year} {brand}. All rights reserved.")
245
+ return f'''function Footer() {{
246
+ return (
247
+ <footer className="border-t {style["border"]} {style["bg"]} px-6 py-10 text-center {style["muted"]}">
248
+ <p>&copy; {footer_text}</p>
249
+ </footer>
250
+ );
251
+ }}'''
252
+
253
+
254
+ def generate_tsx(config: Dict[str, Any]) -> str:
255
+ """Generate complete Next.js/React TSX landing page with Tailwind CSS."""
256
+ style_name = config.get("design_style", "clean-minimal")
257
+ style = DESIGN_STYLES.get(style_name, DESIGN_STYLES["clean-minimal"])
258
+
259
+ components = []
260
+ component_names = []
261
+
262
+ components.append(tsx_nav(config, style))
263
+ component_names.append("Navbar")
264
+
265
+ if config.get("hero"):
266
+ components.append(tsx_hero(config["hero"], style))
267
+ component_names.append("Hero")
268
+
269
+ if config.get("features"):
270
+ components.append(tsx_features(config["features"], style))
271
+ component_names.append("Features")
272
+
273
+ if config.get("testimonials") and config["testimonials"].get("items"):
274
+ components.append(tsx_testimonials(config["testimonials"], style))
275
+ component_names.append("Testimonials")
276
+
277
+ if config.get("pricing") and config["pricing"].get("plans"):
278
+ components.append(tsx_pricing(config["pricing"], style))
279
+ component_names.append("Pricing")
280
+
281
+ if config.get("cta"):
282
+ components.append(tsx_cta(config["cta"], style))
283
+ component_names.append("CTASection")
284
+
285
+ components.append(tsx_footer(config, style))
286
+ component_names.append("Footer")
287
+
288
+ title = config.get("title", "Landing Page")
289
+ meta_desc = config.get("meta_description", "")
290
+
291
+ page_body = "\n ".join(f"<{name} />" for name in component_names)
292
+ all_components = "\n\n".join(components)
293
+
294
+ return f'''// Generated by Landing Page Scaffolder — {datetime.now().strftime("%Y-%m-%d")}
295
+ // Stack: Next.js 14+ App Router, React, Tailwind CSS
296
+ // Design style: {style_name}
297
+
298
+ import type {{ Metadata }} from "next";
299
+
300
+ export const metadata: Metadata = {{
301
+ title: "{title}",
302
+ description: "{meta_desc}",
303
+ openGraph: {{
304
+ title: "{title}",
305
+ description: "{meta_desc}",
306
+ type: "website",
307
+ }},
308
+ }};
309
+
310
+ {all_components}
311
+
312
+ export default function LandingPage() {{
313
+ return (
314
+ <main>
315
+ {page_body}
316
+ </main>
317
+ );
318
+ }}
319
+ '''
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # HTML generators (existing)
324
+ # ---------------------------------------------------------------------------
325
+
326
+ def generate_css(config: Dict[str, Any]) -> str:
327
+ """Generate responsive CSS from config theme."""
328
+ theme = config.get("theme", {})
329
+ primary = theme.get("primary_color", "#2563eb")
330
+ secondary = theme.get("secondary_color", "#1e40af")
331
+ bg = theme.get("background", "#ffffff")
332
+ text_color = theme.get("text_color", "#1f2937")
333
+ font = theme.get("font", "Inter, system-ui, -apple-system, sans-serif")
334
+
335
+ return f"""
336
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
337
+ body {{ font-family: {font}; color: {text_color}; background: {bg}; line-height: 1.6; }}
338
+ .container {{ max-width: 1200px; margin: 0 auto; padding: 0 24px; }}
339
+ nav {{ padding: 16px 0; border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; background: {bg}; z-index: 100; }}
340
+ nav .container {{ display: flex; justify-content: space-between; align-items: center; }}
341
+ .nav-logo {{ font-size: 1.5rem; font-weight: 700; color: {primary}; text-decoration: none; }}
342
+ .nav-links {{ display: flex; gap: 24px; list-style: none; }}
343
+ .nav-links a {{ text-decoration: none; color: {text_color}; font-weight: 500; }}
344
+ .nav-cta {{ background: {primary}; color: white; padding: 8px 20px; border-radius: 6px; text-decoration: none; font-weight: 600; }}
345
+ .hero {{ padding: 80px 0; text-align: center; }}
346
+ .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; }}
347
+ .hero p {{ font-size: 1.25rem; color: #6b7280; max-width: 600px; margin: 0 auto 32px; }}
348
+ .hero-cta {{ display: inline-flex; gap: 16px; }}
349
+ .btn-primary {{ background: {primary}; color: white; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; }}
350
+ .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}; }}
351
+ .features {{ padding: 80px 0; background: #f9fafb; }}
352
+ .section-title {{ text-align: center; font-size: 2.5rem; font-weight: 700; margin-bottom: 16px; }}
353
+ .section-subtitle {{ text-align: center; color: #6b7280; font-size: 1.1rem; margin-bottom: 48px; max-width: 600px; margin-left: auto; margin-right: auto; }}
354
+ .features-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 32px; }}
355
+ .feature-card {{ background: white; padding: 32px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }}
356
+ .feature-icon {{ font-size: 2rem; margin-bottom: 16px; }}
357
+ .feature-card h3 {{ font-size: 1.25rem; margin-bottom: 12px; }}
358
+ .feature-card p {{ color: #6b7280; }}
359
+ .testimonials {{ padding: 80px 0; }}
360
+ .testimonials-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 24px; }}
361
+ .testimonial-card {{ padding: 32px; border: 1px solid #e5e7eb; border-radius: 12px; }}
362
+ .testimonial-text {{ font-size: 1.1rem; font-style: italic; margin-bottom: 20px; }}
363
+ .testimonial-author {{ display: flex; align-items: center; gap: 12px; }}
364
+ .author-info strong {{ display: block; }}
365
+ .author-info span {{ color: #6b7280; font-size: 0.9rem; }}
366
+ .pricing {{ padding: 80px 0; background: #f9fafb; }}
367
+ .pricing-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; max-width: 900px; margin: 0 auto; }}
368
+ .pricing-card {{ background: white; padding: 32px; border-radius: 12px; border: 2px solid #e5e7eb; text-align: center; }}
369
+ .pricing-card.featured {{ border-color: {primary}; position: relative; }}
370
+ .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; }}
371
+ .pricing-name {{ font-size: 1.25rem; font-weight: 600; margin-bottom: 8px; }}
372
+ .pricing-price {{ font-size: 3rem; font-weight: 800; margin: 16px 0; }}
373
+ .pricing-price span {{ font-size: 1rem; font-weight: 400; color: #6b7280; }}
374
+ .pricing-features {{ list-style: none; text-align: left; margin: 24px 0; }}
375
+ .pricing-features li {{ padding: 8px 0; border-bottom: 1px solid #f3f4f6; }}
376
+ .pricing-features li::before {{ content: "\\2713 "; color: {primary}; font-weight: 700; }}
377
+ .cta-section {{ padding: 80px 0; text-align: center; background: {primary}; color: white; }}
378
+ .cta-section h2 {{ font-size: 2.5rem; margin-bottom: 16px; }}
379
+ .cta-section p {{ font-size: 1.1rem; opacity: 0.9; margin-bottom: 32px; }}
380
+ .btn-white {{ background: white; color: {primary}; padding: 14px 32px; border-radius: 8px; text-decoration: none; font-weight: 600; font-size: 1.1rem; }}
381
+ footer {{ padding: 40px 0; border-top: 1px solid #e5e7eb; color: #6b7280; text-align: center; }}
382
+ @media (max-width: 768px) {{
383
+ .hero h1 {{ font-size: 2.25rem; }}
384
+ .hero-cta {{ flex-direction: column; align-items: center; }}
385
+ .nav-links {{ display: none; }}
386
+ .features-grid {{ grid-template-columns: 1fr; }}
387
+ .pricing-grid {{ grid-template-columns: 1fr; }}
388
+ }}
389
+ """
390
+
391
+
392
+ def render_nav(config: Dict[str, Any]) -> str:
393
+ brand = escape(config.get("brand", "Brand"))
394
+ nav_links = config.get("nav_links", [])
395
+ cta = config.get("nav_cta", {"text": "Get Started", "url": "#"})
396
+ links = "\n".join(
397
+ f'<li><a href="{escape(l.get("url", "#"))}">{escape(l.get("text", ""))}</a></li>'
398
+ for l in nav_links
399
+ )
400
+ return f"""
401
+ <nav><div class="container">
402
+ <a href="#" class="nav-logo">{brand}</a>
403
+ <ul class="nav-links">{links}</ul>
404
+ <a href="{escape(cta.get('url', '#'))}" class="nav-cta">{escape(cta.get('text', 'Get Started'))}</a>
405
+ </div></nav>"""
406
+
407
+
408
+ def render_hero(hero: Dict[str, Any]) -> str:
409
+ h1 = escape(hero.get("headline", "Your Headline Here"))
410
+ sub = escape(hero.get("subheadline", ""))
411
+ primary_cta = hero.get("primary_cta", {"text": "Get Started", "url": "#"})
412
+ secondary_cta = hero.get("secondary_cta", None)
413
+ cta_html = f'<a href="{escape(primary_cta.get("url", "#"))}" class="btn-primary">{escape(primary_cta.get("text", "Get Started"))}</a>'
414
+ if secondary_cta:
415
+ cta_html += f'\n<a href="{escape(secondary_cta.get("url", "#"))}" class="btn-secondary">{escape(secondary_cta.get("text", "Learn More"))}</a>'
416
+ return f"""
417
+ <section class="hero"><div class="container">
418
+ <h1>{h1}</h1>
419
+ <p>{sub}</p>
420
+ <div class="hero-cta">{cta_html}</div>
421
+ </div></section>"""
422
+
423
+
424
+ def render_features(features: Dict[str, Any]) -> str:
425
+ title = escape(features.get("title", "Features"))
426
+ subtitle = escape(features.get("subtitle", ""))
427
+ items = features.get("items", [])
428
+ cards = "\n".join(f"""
429
+ <div class="feature-card">
430
+ <div class="feature-icon">{escape(f.get('icon', ''))}</div>
431
+ <h3>{escape(f.get('title', ''))}</h3>
432
+ <p>{escape(f.get('description', ''))}</p>
433
+ </div>""" for f in items)
434
+ return f"""
435
+ <section class="features"><div class="container">
436
+ <h2 class="section-title">{title}</h2>
437
+ <p class="section-subtitle">{subtitle}</p>
438
+ <div class="features-grid">{cards}</div>
439
+ </div></section>"""
440
+
441
+
442
+ def render_testimonials(testimonials: Dict[str, Any]) -> str:
443
+ title = escape(testimonials.get("title", "What Our Customers Say"))
444
+ items = testimonials.get("items", [])
445
+ if not items:
446
+ return ""
447
+ cards = "\n".join(f"""
448
+ <div class="testimonial-card">
449
+ <p class="testimonial-text">"{escape(t.get('quote', ''))}"</p>
450
+ <div class="testimonial-author">
451
+ <div class="author-info">
452
+ <strong>{escape(t.get('name', ''))}</strong>
453
+ <span>{escape(t.get('title', ''))}, {escape(t.get('company', ''))}</span>
454
+ </div>
455
+ </div>
456
+ </div>""" for t in items)
457
+ return f"""
458
+ <section class="testimonials"><div class="container">
459
+ <h2 class="section-title">{title}</h2>
460
+ <div class="testimonials-grid">{cards}</div>
461
+ </div></section>"""
462
+
463
+
464
+ def render_pricing(pricing: Dict[str, Any]) -> str:
465
+ title = escape(pricing.get("title", "Pricing"))
466
+ plans = pricing.get("plans", [])
467
+ if not plans:
468
+ return ""
469
+ cards = "\n".join(f"""
470
+ <div class="pricing-card {'featured' if p.get('featured') else ''}">
471
+ <div class="pricing-name">{escape(p.get('name', ''))}</div>
472
+ <div class="pricing-price">${escape(str(p.get('price', '0')))}<span>/mo</span></div>
473
+ <p>{escape(p.get('description', ''))}</p>
474
+ <ul class="pricing-features">
475
+ {"".join(f'<li>{escape(f)}</li>' for f in p.get('features', []))}
476
+ </ul>
477
+ <a href="{escape(p.get('cta_url', '#'))}" class="btn-primary">{escape(p.get('cta_text', 'Choose Plan'))}</a>
478
+ </div>""" for p in plans)
479
+ return f"""
480
+ <section class="pricing"><div class="container">
481
+ <h2 class="section-title">{title}</h2>
482
+ <div class="pricing-grid">{cards}</div>
483
+ </div></section>"""
484
+
485
+
486
+ def render_cta(cta: Dict[str, Any]) -> str:
487
+ return f"""
488
+ <section class="cta-section"><div class="container">
489
+ <h2>{escape(cta.get('headline', 'Ready to get started?'))}</h2>
490
+ <p>{escape(cta.get('subheadline', ''))}</p>
491
+ <a href="{escape(cta.get('url', '#'))}" class="btn-white">{escape(cta.get('text', 'Start Free Trial'))}</a>
492
+ </div></section>"""
493
+
494
+
495
+ def generate_html(config: Dict[str, Any]) -> str:
496
+ """Generate complete HTML landing page."""
497
+ title = escape(config.get("title", "Landing Page"))
498
+ css = generate_css(config)
499
+ sections = []
500
+ sections.append(render_nav(config))
501
+ if config.get("hero"):
502
+ sections.append(render_hero(config["hero"]))
503
+ if config.get("features"):
504
+ sections.append(render_features(config["features"]))
505
+ if config.get("testimonials"):
506
+ sections.append(render_testimonials(config["testimonials"]))
507
+ if config.get("pricing"):
508
+ sections.append(render_pricing(config["pricing"]))
509
+ if config.get("cta"):
510
+ sections.append(render_cta(config["cta"]))
511
+ sections.append(f"""
512
+ <footer><div class="container">
513
+ <p>{escape(config.get('footer_text', f'{datetime.now().year} {config.get("brand", "Company")}. All rights reserved.'))}</p>
514
+ </div></footer>""")
515
+ return f"""<!DOCTYPE html>
516
+ <html lang="en">
517
+ <head>
518
+ <meta charset="UTF-8">
519
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
520
+ <title>{title}</title>
521
+ <meta name="description" content="{escape(config.get('meta_description', ''))}">
522
+ <style>{css}</style>
523
+ </head>
524
+ <body>
525
+ {"".join(sections)}
526
+ </body>
527
+ </html>"""
528
+
529
+
530
+ def main():
531
+ parser = argparse.ArgumentParser(
532
+ description="Generate landing pages as HTML or Next.js TSX with Tailwind CSS"
533
+ )
534
+ parser.add_argument("input", help="Path to page config JSON")
535
+ parser.add_argument(
536
+ "--format", choices=["html", "tsx", "json"], default="tsx",
537
+ help="Output format: tsx (Next.js + Tailwind), html (standalone), json (metadata)"
538
+ )
539
+ parser.add_argument("--output", type=str, default=None, help="Output file path")
540
+
541
+ args = parser.parse_args()
542
+
543
+ with open(args.input) as f:
544
+ config = json.load(f)
545
+
546
+ if args.format == "json":
547
+ output = json.dumps({
548
+ "generated_at": datetime.now().isoformat(),
549
+ "config": config,
550
+ "formats_available": ["html", "tsx"],
551
+ "sections": [k for k in ["nav", "hero", "features", "testimonials", "pricing", "cta", "footer"]
552
+ if config.get(k) or k in ("nav", "footer")]
553
+ }, indent=2)
554
+ elif args.format == "tsx":
555
+ output = generate_tsx(config)
556
+ else:
557
+ output = generate_html(config)
558
+
559
+ if args.output:
560
+ with open(args.output, "w") as f:
561
+ f.write(output)
562
+ print(f"Landing page written to {args.output}")
563
+ else:
564
+ print(output)
565
+
566
+
567
+ if __name__ == "__main__":
568
+ main()