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.
- package/README.md +5 -3
- package/bundled-skills/007/scripts/full_audit.py +6 -4
- package/bundled-skills/007/scripts/score_calculator.py +67 -7
- package/bundled-skills/algorithmic-art/templates/viewer.html +2 -2
- package/bundled-skills/apify-actorization/SKILL.md +1 -2
- package/bundled-skills/apify-actorization/references/cli-actorization.md +4 -4
- package/bundled-skills/docs/COMMUNITY_GUIDELINES.md +1 -1
- package/bundled-skills/docs/contributors/community-guidelines.md +3 -32
- package/bundled-skills/docs/integrations/jetski-gemini-loader/loader.ts +21 -3
- package/bundled-skills/docs/maintainers/security-findings-triage-2026-03-18-addendum.md +22 -0
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/walkthrough.md +21 -17
- package/bundled-skills/dotnet-backend-patterns/resources/implementation-playbook.md +2 -2
- package/bundled-skills/instagram/scripts/auth.py +15 -6
- package/bundled-skills/landing-page-generator/SKILL.md +203 -0
- package/bundled-skills/landing-page-generator/references/conversion-patterns.md +176 -0
- package/bundled-skills/landing-page-generator/references/frameworks.md +177 -0
- package/bundled-skills/landing-page-generator/references/landing-page-patterns.md +98 -0
- package/bundled-skills/landing-page-generator/references/seo-checklist.md +109 -0
- package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +568 -0
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +33 -1073
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +7 -4
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/migrations.ts +15 -3
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/routes/todos.ts +85 -88
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package-lock.json +260 -456
- package/bundled-skills/loki-mode/examples/todo-app-generated/frontend/package.json +4 -2
- package/bundled-skills/notebooklm/scripts/auth_manager.py +17 -3
- package/bundled-skills/notebooklm/scripts/browser_session.py +11 -2
- package/bundled-skills/radix-ui-design-system/examples/README.md +1 -1
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/webhook-handler.ts +5 -3
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/app.py +21 -13
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/webhook_handler.py +11 -4
- package/package.json +1 -1
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/db/db.ts +0 -35
- /package/bundled-skills/dotnet-backend-patterns/assets/{repository-template.cs → repository-template.cs.template} +0 -0
- /package/bundled-skills/dotnet-backend-patterns/assets/{service-template.cs → service-template.cs.template} +0 -0
- /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">✓</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>© {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()
|