
Micro Landing Builder
- 147 installs
- 31 repo stars
- Updated August 2, 2026
- shipshitdev/library
Ship a focused single-page landing with hero, proof, and CTA fast to test positioning, capture waitlist signups, and measure demand before full product build.
About
Micro-landing-builder scaffolds small, conversion-focused landing pages—hero, benefits, proof, and signup CTA—so teams can validate positioning and demand for SaaS, content, or ecommerce ideas before investing in a full application.
- Produces tight single-page layouts with clear CTA
- Supports waitlist, demo, and early-access capture flows
- Emphasizes fast copy-test and positioning iteration
- Keeps scope minimal for quick deploy and analytics
- Pairs structure with social proof and value props
Micro Landing Builder by the numbers
- 147 all-time installs (skills.sh)
- +3 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #961 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/shipshitdev/library --skill micro-landing-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 147 |
|---|---|
| repo stars | ★ 31 |
| Last updated | August 2, 2026 |
| Repository | shipshitdev/library ↗ |
What it does
Ship a focused single-page landing with hero, proof, and CTA fast to test positioning, capture waitlist signups, and measure demand before full product build.
Files
Micro Landing Builder
Create config-driven NextJS landing pages for startups.
Contract
Inputs:
- One or more landing definitions: slug, name, domain, concept, and config
- Destination root
- Optional shared UI package and domain mapping
Outputs:
- Generated landing app directories
app.jsonconfig files- Deployment plan or deployed URLs when deploy is explicitly requested
Creates/Modifies:
- Local landing app directories
- Vercel config files
- Does not deploy production by default
External Side Effects:
- May deploy to Vercel and attach custom domains only after explicit deploy request
Confirmation Required:
- Before batch creation outside the current workspace
- Before production deploys
- Before attaching custom domains
- Before overwriting an existing landing directory
Delegates To:
landing-page-vercelfor single static landing pagesproject-init-orchestrator/npx @shipshitdev/v0for full product reposdeployment-composerordeployfor Vercel deployment
Concept
Each landing page is a standalone NextJS app where:
- Content is defined in
app.jsonconfig file - UI comes from
@agenticindiedev/ui - Deploy independently to any domain via Vercel
Prerequisites
You need a published landing UI components package. The skill expects:
- Package name (default:
@agenticindiedev/ui) - Components: Hero, Features, Pricing, FAQ, CTA, Testimonials, Stats, EmailCapture, Header, Footer
Usage
# Show help
python3 scripts/scaffold.py --help
# Create a new landing
python3 scripts/scaffold.py \
--slug mystartup \
--name "My Startup" \
--domain "mystartup.com" \
--concept "AI-powered analytics"
# With custom UI package
python3 scripts/scaffold.py \
--slug mystartup \
--name "My Startup" \
--ui-package "@myorg/landing-kit"
# Allow outside current directory
python3 scripts/scaffold.py \
--root ~/www/landings \
--slug mystartup \
--allow-outsideGenerated Structure
mystartup/
├── app.json # All content/config here
├── package.json # Depends on UI package
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── vercel.json # Vercel deployment config
├── public/
│ └── (images go here)
└── app/
├── layout.tsx
├── page.tsx # Renders sections from app.json
└── globals.cssapp.json Config
The landing is entirely driven by app.json. See references/config-schema.md for full schema.
{
"name": "My Startup",
"slug": "mystartup",
"domain": "mystartup.com",
"meta": {
"title": "My Startup - Tagline",
"description": "SEO description"
},
"theme": {
"primary": "#6366f1",
"accent": "#f59e0b"
},
"analytics": {
"plausible": "mystartup.com"
},
"sections": [
{ "type": "hero", "headline": "...", "subheadline": "..." },
{ "type": "features", "items": [...] },
{ "type": "pricing", "plans": [...] },
{ "type": "faq", "items": [...] },
{ "type": "cta", "emailCapture": { "enabled": true } }
]
}Section Types
hero- Main hero with headline, CTA buttonsstats- Key metrics/numbersfeatures- Feature grid with iconspricing- Pricing planstestimonials- Customer quotesfaq- Accordion FAQcta- Call to action with email capture
Batch Creation
Create multiple landing pages from a template or CSV/JSON file:
# From CSV file
python3 scripts/batch_create.py \
--root ~/www/landings \
--csv projects.csv \
--allow-outside
# From JSON file
python3 scripts/batch_create.py \
--root ~/www/landings \
--json projects.json \
--allow-outside
# Clone from existing template
python3 scripts/batch_create.py \
--root ~/www/landings \
--template ~/www/landings/template-landing \
--json projects.json \
--allow-outsideCSV Format
slug,name,domain,concept
project1,Project One,project1.com,AI-powered analytics
project2,Project Two,project2.com,Cloud infrastructureJSON Format
[
{
"slug": "project1",
"name": "Project One",
"domain": "project1.com",
"concept": "AI-powered analytics"
},
{
"slug": "project2",
"name": "Project Two",
"domain": "project2.com",
"concept": "Cloud infrastructure"
}
]Deployment
Single Project
Before running vercel, confirm .vercel/project.json exists in the landing directory. If it does not exist, stop and ask the user to run vercel link manually — do not run it unattended.
cd mystartup
vercelBatch Deployment with Domains
Deploy multiple projects to Vercel with custom domains:
# Deploy with domain mapping
python3 scripts/deploy_vercel.py \
~/www/landings/project1 \
~/www/landings/project2 \
--domains-json domains.json \
--prod \
--yes
# Single domain
python3 scripts/deploy_vercel.py \
~/www/landings/project1 \
--domain project1.com \
--prod \
--yesDomain Mapping JSON
{
"project1": "project1.com",
"project2": "project2.com"
}Note: Domains must be configured in your DNS before adding to Vercel. Vercel will provide DNS records to add.
Workflow
Single Landing Page
1. Run scaffold to create landing structure 2. Edit app.json with your content 3. Add images to public/ 4. Deploy with vercel or use deploy_vercel.py
Multiple Landing Pages
1. Create CSV/JSON file with project definitions 2. Run batch_create.py to generate all landing pages 3. Customize each app.json as needed 4. Run deploy_vercel.py to deploy all with domains
Customization
To add custom sections or override components:
1. Add component to app/components/ 2. Import in app/page.tsx 3. Add to section renderer
References
references/config-schema.md- Full JSON schemareferences/sections-reference.md- Section types and props
{
"project1": "project1.com",
"project2": "project2.com",
"project3": "project3.com"
}
slug,name,domain,concept
project1,Project One,project1.com,AI-powered analytics
project2,Project Two,project2.com,Cloud infrastructure
project3,Project Three,project3.com,Data visualization
[
{
"slug": "project1",
"name": "Project One",
"domain": "project1.com",
"concept": "AI-powered analytics"
},
{
"slug": "project2",
"name": "Project Two",
"domain": "project2.com",
"concept": "Cloud infrastructure"
},
{
"slug": "project3",
"name": "Project Three",
"domain": "project3.com",
"concept": "Data visualization"
}
]
{
"name": "micro-landing-builder",
"version": "1.0.0",
"description": "Scaffold, clone, and deploy config-driven NextJS landing pages that use a shared UI components packa",
"author": {
"name": "Ship Shit Dev",
"email": "hello@shipshit.dev",
"url": "https://shipshit.dev"
},
"license": "MIT",
"skills": "."
}
app.json Config Schema
Complete schema for the landing page configuration file.
---
Root Schema
interface AppConfig {
name: string; // Display name
slug: string; // URL-friendly identifier
domain: string; // Domain for analytics/SEO
meta: MetaConfig;
theme: ThemeConfig;
analytics: AnalyticsConfig;
header: HeaderConfig;
sections: Section[];
footer: FooterConfig;
}---
Meta Config
interface MetaConfig {
title: string; // Page title
description: string; // Meta description for SEO
ogImage?: string; // Open Graph image path
}---
Theme Config
interface ThemeConfig {
primary: string; // Primary color (hex)
accent: string; // Accent color (hex)
background: string; // Background color (hex)
font: {
heading: string; // Font for headings
body: string; // Font for body text
};
}Recommended fonts:
- Headings:
Fraunces,Playfair Display,Clash Display - Body:
Space Grotesk,Inter,DM Sans
---
Analytics Config
interface AnalyticsConfig {
plausible?: string; // Plausible domain
ga?: string; // Google Analytics ID (G-XXXXX)
}---
Header Config
interface HeaderConfig {
logo: {
mark: string; // Short text/initials (e.g., "MS")
text: string; // Full name
image?: string; // Optional logo image path
};
nav: NavItem[];
cta: CTAButton;
}
interface NavItem {
label: string;
href: string;
}
interface CTAButton {
label: string;
href: string;
}---
Sections
Array of section objects. Each has a type field.
Hero Section
interface HeroSection {
type: "hero";
eyebrow?: string; // Small text above headline
headline: string;
subheadline: string;
badges?: string[]; // e.g., ["YC W24", "Product Hunt #1"]
primaryCta: CTAButton;
secondaryCta?: CTAButton;
image?: string; // Hero image path
note?: string; // Small note below CTAs
}Stats Section
interface StatsSection {
type: "stats";
items: StatItem[];
}
interface StatItem {
value: string; // e.g., "10K+"
label: string; // e.g., "Users"
}Features Section
interface FeaturesSection {
type: "features";
title: string;
subtitle?: string;
items: FeatureItem[];
}
interface FeatureItem {
icon: string; // Icon name (e.g., "zap", "shield")
title: string;
description: string;
}Available icons: zap, shield, trending-up, users, lock, globe, check, star, heart, settings
Pricing Section
interface PricingSection {
type: "pricing";
title: string;
subtitle?: string;
plans: PricingPlan[];
}
interface PricingPlan {
name: string;
price: {
monthly: number;
yearly: number;
};
description?: string;
features: string[];
cta: CTAButton;
highlighted?: boolean; // Visual emphasis
}Testimonials Section
interface TestimonialsSection {
type: "testimonials";
title: string;
items: Testimonial[];
}
interface Testimonial {
quote: string;
author: string;
role: string; // e.g., "CEO at Company"
avatar?: string; // Avatar image path
}FAQ Section
interface FAQSection {
type: "faq";
title: string;
items: FAQItem[];
}
interface FAQItem {
q: string; // Question
a: string; // Answer
}CTA Section
interface CTASection {
type: "cta";
headline: string;
subheadline?: string;
emailCapture?: {
enabled: boolean;
provider: "resend" | "mailchimp";
placeholder?: string;
buttonText?: string;
};
cta?: CTAButton; // Alternative to email capture
}---
Footer Config
interface FooterConfig {
links: FooterLink[];
social: SocialLink[];
copyright: string;
}
interface FooterLink {
label: string;
href: string;
}
interface SocialLink {
platform: "twitter" | "github" | "linkedin" | "instagram";
href: string;
}---
Complete Example
{
"name": "My Startup",
"slug": "mystartup",
"domain": "mystartup.com",
"meta": {
"title": "My Startup - AI Analytics",
"description": "Transform your analytics with AI",
"ogImage": "/og.png"
},
"theme": {
"primary": "#6366f1",
"accent": "#f59e0b",
"background": "#0a0a0a",
"font": {
"heading": "Fraunces",
"body": "Space Grotesk"
}
},
"analytics": {
"plausible": "mystartup.com"
},
"header": {
"logo": { "mark": "MS", "text": "My Startup" },
"nav": [
{ "label": "Features", "href": "#features" },
{ "label": "Pricing", "href": "#pricing" }
],
"cta": { "label": "Get Started", "href": "#signup" }
},
"sections": [
{
"type": "hero",
"headline": "Analytics, reimagined",
"subheadline": "AI-powered insights for modern teams",
"primaryCta": { "label": "Start Free", "href": "#signup" }
},
{
"type": "features",
"title": "Features",
"items": [
{ "icon": "zap", "title": "Fast", "description": "Lightning speed" }
]
}
],
"footer": {
"links": [{ "label": "Privacy", "href": "/privacy" }],
"social": [{ "platform": "twitter", "href": "https://twitter.com/..." }],
"copyright": "2024 My Startup"
}
}Sections Reference
Quick reference for available section types and their props.
---
Section Order Recommendations
Typical landing page flow:
1. hero - First impression, main value prop 2. stats - Social proof numbers (optional) 3. features - What the product does 4. pricing - How much it costs 5. testimonials - Social proof quotes 6. faq - Address objections 7. cta - Final conversion
---
Hero
The main hero section at the top of the page.
{
"type": "hero",
"eyebrow": "Now in beta",
"headline": "Your main headline",
"subheadline": "Supporting text that explains the value",
"badges": ["YC W24", "Product Hunt #1"],
"primaryCta": { "label": "Get Started", "href": "#signup" },
"secondaryCta": { "label": "Learn More", "href": "#features" },
"image": "/hero.png",
"note": "No credit card required"
}Tips:
- Keep headline under 10 words
- Subheadline should explain the "how"
- Include social proof badges if available
---
Stats
Key metrics that build credibility.
{
"type": "stats",
"items": [
{ "value": "10K+", "label": "Users" },
{ "value": "99.9%", "label": "Uptime" },
{ "value": "4.9", "label": "Rating" },
{ "value": "50M+", "label": "Requests" }
]
}Tips:
- Use 3-4 stats maximum
- Include units in value (K, M, %)
- Focus on impressive numbers
---
Features
Showcase product capabilities.
{
"type": "features",
"title": "Everything you need",
"subtitle": "Powerful features for modern teams",
"items": [
{
"icon": "zap",
"title": "Lightning Fast",
"description": "Sub-millisecond response times"
},
{
"icon": "shield",
"title": "Secure",
"description": "Enterprise-grade security"
},
{
"icon": "trending-up",
"title": "Analytics",
"description": "Real-time insights"
}
]
}Available icons:
zap- Speed/performanceshield- Securitytrending-up- Growth/analyticsusers- Team/collaborationlock- Privacyglobe- Global/internationalcheck- Verification/successstar- Quality/premiumheart- Favorites/lovesettings- Customization
Tips:
- Use 3, 6, or 9 features for best grid layout
- Keep descriptions under 15 words
---
Pricing
Display pricing plans.
{
"type": "pricing",
"title": "Simple pricing",
"subtitle": "No hidden fees",
"plans": [
{
"name": "Free",
"price": { "monthly": 0, "yearly": 0 },
"description": "For individuals",
"features": ["1,000 requests", "Basic support"],
"cta": { "label": "Start Free", "href": "#" }
},
{
"name": "Pro",
"price": { "monthly": 29, "yearly": 290 },
"description": "For teams",
"features": ["Unlimited requests", "Priority support"],
"cta": { "label": "Get Pro", "href": "#" },
"highlighted": true
}
]
}Tips:
- Always include a free tier
- Highlight the recommended plan
- Keep feature lists scannable (5-7 items)
---
Testimonials
Social proof from customers.
{
"type": "testimonials",
"title": "Loved by teams",
"items": [
{
"quote": "This product changed everything for us.",
"author": "Jane Doe",
"role": "CEO at TechCorp",
"avatar": "/avatars/jane.jpg"
}
]
}Tips:
- Include real names and roles
- Keep quotes under 30 words
- Use avatars when available
---
FAQ
Address common questions and objections.
{
"type": "faq",
"title": "Frequently asked questions",
"items": [
{
"q": "Is there a free trial?",
"a": "Yes, our Free plan is free forever."
},
{
"q": "Can I cancel anytime?",
"a": "Yes, no long-term contracts."
}
]
}Tips:
- Address pricing objections
- Include technical questions
- Keep answers concise
---
CTA
Final call to action.
{
"type": "cta",
"headline": "Ready to get started?",
"subheadline": "Join thousands of happy users",
"emailCapture": {
"enabled": true,
"provider": "resend",
"placeholder": "Enter your email",
"buttonText": "Join Waitlist"
}
}Or with button instead of email:
{
"type": "cta",
"headline": "Start your free trial",
"cta": { "label": "Get Started", "href": "/signup" }
}Tips:
- Create urgency in headline
- Email capture for waitlists
- Direct CTA for live products
#!/usr/bin/env python3
"""
Batch create multiple landing pages from a template or CSV/JSON file.
"""
from __future__ import annotations
import argparse
import csv
import json
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any
def load_projects_from_csv(csv_path: Path) -> list[dict[str, Any]]:
"""Load project definitions from CSV file."""
projects = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
projects.append({
"slug": row.get("slug", "").strip(),
"name": row.get("name", "").strip(),
"domain": row.get("domain", "").strip(),
"concept": row.get("concept", "").strip(),
})
return projects
def load_projects_from_json(json_path: Path) -> list[dict[str, Any]]:
"""Load project definitions from JSON file."""
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return data
elif isinstance(data, dict) and "projects" in data:
return data["projects"]
else:
raise ValueError("JSON must be an array or object with 'projects' key")
def clone_from_template(
template_dir: Path,
target_dir: Path,
slug: str,
name: str,
domain: str,
concept: str,
) -> None:
"""Clone a landing page from template and update config."""
if not template_dir.exists():
raise FileNotFoundError(f"Template directory not found: {template_dir}")
# Copy template
shutil.copytree(template_dir, target_dir, ignore=shutil.ignore_patterns(
"node_modules", ".next", ".vercel", ".git"
))
# Update app.json
app_json_path = target_dir / "app.json"
if app_json_path.exists():
with open(app_json_path, "r", encoding="utf-8") as f:
config = json.load(f)
config["name"] = name
config["slug"] = slug
config["domain"] = domain
config["meta"]["title"] = f"{name} - {concept}"
config["meta"]["description"] = f"{name}: {concept}. Join thousands of users."
with open(app_json_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2)
# Update package.json name
package_json_path = target_dir / "package.json"
if package_json_path.exists():
with open(package_json_path, "r", encoding="utf-8") as f:
package = json.load(f)
package["name"] = slug
with open(package_json_path, "w", encoding="utf-8") as f:
json.dump(package, f, indent=2)
print(f"✅ Cloned and configured: {target_dir}")
def create_from_scaffold(
root: Path,
slug: str,
name: str,
domain: str,
concept: str,
ui_package: str,
scaffold_script: Path,
) -> None:
"""Create a new landing page using scaffold script."""
cmd = [
"python3",
str(scaffold_script),
"--root", str(root),
"--slug", slug,
"--name", name,
"--domain", domain,
"--concept", concept,
"--ui-package", ui_package,
"--allow-outside",
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ Failed to create {slug}: {result.stderr}", file=sys.stderr)
raise RuntimeError(f"Scaffold failed for {slug}")
print(result.stdout)
def batch_create(
root: Path,
projects: list[dict[str, Any]],
template_dir: Path | None,
ui_package: str,
scaffold_script: Path,
allow_outside: bool,
) -> None:
"""Create multiple landing pages."""
cwd = Path.cwd()
if not allow_outside and not root.is_relative_to(cwd):
print(f"Error: Target path {root} is outside current directory.")
print("Use --allow-outside to confirm this is intentional.")
sys.exit(1)
root.mkdir(parents=True, exist_ok=True)
created = []
failed = []
for project in projects:
slug = project.get("slug", "").strip()
name = project.get("name", "").strip()
domain = project.get("domain", "").strip()
concept = project.get("concept", "innovative solution").strip()
if not slug or not name:
print(f"⚠️ Skipping invalid project: {project}")
failed.append(project)
continue
target_dir = root / slug
if target_dir.exists():
print(f"⚠️ Skipping {slug}: already exists")
continue
try:
if template_dir and template_dir.exists():
clone_from_template(template_dir, target_dir, slug, name, domain, concept)
else:
create_from_scaffold(
root, slug, name, domain, concept, ui_package, scaffold_script
)
created.append(slug)
except Exception as e:
print(f"❌ Failed to create {slug}: {e}", file=sys.stderr)
failed.append(project)
print(f"\n📊 Summary:")
print(f"✅ Created: {len(created)}")
print(f"❌ Failed: {len(failed)}")
if created:
print(f"\nCreated projects: {', '.join(created)}")
if failed:
print(f"\nFailed projects: {[p.get('slug', 'unknown') for p in failed]}")
def main() -> None:
parser = argparse.ArgumentParser(
description="Batch create multiple landing pages from template or CSV/JSON."
)
parser.add_argument(
"--root",
type=Path,
default=Path.cwd(),
help="Parent directory for landings (default: current directory)",
)
parser.add_argument(
"--template",
type=Path,
help="Template landing page directory to clone from",
)
parser.add_argument(
"--csv",
type=Path,
help="CSV file with columns: slug,name,domain,concept",
)
parser.add_argument(
"--json",
type=Path,
help="JSON file with array of {slug, name, domain, concept} objects",
)
parser.add_argument(
"--ui-package",
type=str,
default="@agenticindiedev/ui",
help="UI components package (default: @agenticindiedev/ui)",
)
parser.add_argument(
"--allow-outside",
action="store_true",
help="Allow creating files outside current directory",
)
args = parser.parse_args()
# Determine projects source
if args.csv:
projects = load_projects_from_csv(args.csv)
elif args.json:
projects = load_projects_from_json(args.json)
else:
print("Error: Must provide --csv or --json file", file=sys.stderr)
sys.exit(1)
if not projects:
print("Error: No projects found in input file", file=sys.stderr)
sys.exit(1)
# Get scaffold script path
skill_dir = Path(__file__).parent.parent
scaffold_script = skill_dir / "scripts" / "scaffold.py"
batch_create(
root=args.root.resolve(),
projects=projects,
template_dir=args.template.resolve() if args.template else None,
ui_package=args.ui_package,
scaffold_script=scaffold_script,
allow_outside=args.allow_outside,
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Deploy landing pages to Vercel with custom domain configuration.
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Any
def check_vercel_cli() -> bool:
"""Check if Vercel CLI is installed."""
try:
subprocess.run(["vercel", "--version"], capture_output=True, check=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def get_vercel_project_id(project_dir: Path) -> str | None:
"""Get Vercel project ID from .vercel directory."""
vercel_dir = project_dir / ".vercel"
project_json = vercel_dir / "project.json"
if project_json.exists():
with open(project_json, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("projectId")
return None
def deploy_project(
project_dir: Path,
domain: str | None,
production: bool,
yes: bool,
) -> dict[str, Any]:
"""Deploy a single project to Vercel."""
if not project_dir.exists():
raise FileNotFoundError(f"Project directory not found: {project_dir}")
cmd = ["vercel", "--yes"] if yes else ["vercel"]
if production:
cmd.append("--prod")
result = subprocess.run(
cmd,
cwd=project_dir,
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"Deployment failed: {result.stderr}")
# Extract deployment URL from output
output = result.stdout
deployment_url = None
for line in output.split("\n"):
if "https://" in line and ".vercel.app" in line:
deployment_url = line.strip().split()[-1]
break
project_id = get_vercel_project_id(project_dir)
result_info = {
"project_dir": str(project_dir),
"deployment_url": deployment_url,
"project_id": project_id,
"domain": domain,
}
# Add domain if provided
if domain and project_id:
try:
add_domain_cmd = [
"vercel",
"domains",
"add",
domain,
"--yes",
]
domain_result = subprocess.run(
add_domain_cmd,
cwd=project_dir,
capture_output=True,
text=True,
)
if domain_result.returncode == 0:
result_info["domain_added"] = True
result_info["domain_status"] = "added"
else:
result_info["domain_added"] = False
result_info["domain_error"] = domain_result.stderr
except Exception as e:
result_info["domain_added"] = False
result_info["domain_error"] = str(e)
return result_info
def batch_deploy(
projects: list[Path],
domains: dict[str, str] | None,
production: bool,
yes: bool,
) -> list[dict[str, Any]]:
"""Deploy multiple projects to Vercel."""
results = []
for project_dir in projects:
project_dir = project_dir.resolve()
slug = project_dir.name
domain = domains.get(slug) if domains else None
print(f"\n🚀 Deploying {slug}...")
if domain:
print(f" Domain: {domain}")
try:
result = deploy_project(project_dir, domain, production, yes)
results.append(result)
print(f"✅ Deployed: {slug}")
if result.get("deployment_url"):
print(f" URL: {result['deployment_url']}")
if result.get("domain_added"):
print(f" Domain: {domain} (added)")
except Exception as e:
print(f"❌ Failed: {slug} - {e}", file=sys.stderr)
results.append({
"project_dir": str(project_dir),
"error": str(e),
})
return results
def load_domains_from_json(json_path: Path) -> dict[str, str]:
"""Load domain mapping from JSON file."""
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
return data
elif isinstance(data, list):
return {item.get("slug", ""): item.get("domain", "") for item in data}
else:
raise ValueError("JSON must be an object or array")
def main() -> None:
parser = argparse.ArgumentParser(
description="Deploy landing pages to Vercel with custom domains."
)
parser.add_argument(
"projects",
nargs="+",
type=Path,
help="Project directories to deploy",
)
parser.add_argument(
"--domain",
type=str,
help="Single domain to assign to first project",
)
parser.add_argument(
"--domains-json",
type=Path,
help="JSON file mapping slugs to domains: {\"slug\": \"domain.com\"}",
)
parser.add_argument(
"--prod",
action="store_true",
help="Deploy to production",
)
parser.add_argument(
"--yes",
action="store_true",
help="Skip confirmation prompts",
)
args = parser.parse_args()
# Check Vercel CLI
if not check_vercel_cli():
print("Error: Vercel CLI not found. Install with: npm i -g vercel", file=sys.stderr)
sys.exit(1)
# Load domains
domains = {}
if args.domains_json:
domains = load_domains_from_json(args.domains_json)
elif args.domain:
# Assign to first project
if args.projects:
domains[args.projects[0].name] = args.domain
# Deploy
results = batch_deploy(
projects=args.projects,
domains=domains if domains else None,
production=args.prod,
yes=args.yes,
)
# Summary
print("\n" + "=" * 50)
print("📊 Deployment Summary")
print("=" * 50)
for result in results:
if "error" in result:
print(f"❌ {Path(result['project_dir']).name}: {result['error']}")
else:
print(f"✅ {Path(result['project_dir']).name}")
if result.get("deployment_url"):
print(f" URL: {result['deployment_url']}")
if result.get("domain"):
print(f" Domain: {result['domain']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Scaffold a config-driven NextJS landing page.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from textwrap import dedent
SKILL_DIR = Path(__file__).parent.parent
TEMPLATES_DIR = SKILL_DIR / "assets" / "templates" / "landing"
DEFAULT_UI_PACKAGE = "@agenticindiedev/ui"
def create_package_json(name: str, ui_package: str) -> str:
return json.dumps({
"name": name.lower().replace(" ", "-"),
"version": "0.1.0",
"private": True,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@agenticindiedev/ui": "latest"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0",
"tailwindcss": "^4.0.0",
"@tailwindcss/postcss": "^4.0.0"
}
}, indent=2)
def create_next_config() -> str:
return dedent("""\
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;
""")
def create_tailwind_config(ui_package: str) -> str:
template = dedent("""\
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/__UI_PACKAGE__/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
export default config;
""")
return template.replace("__UI_PACKAGE__", ui_package)
def create_tsconfig() -> str:
return json.dumps({
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": True,
"skipLibCheck": True,
"strict": True,
"noEmit": True,
"esModuleInterop": True,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": True,
"isolatedModules": True,
"jsx": "preserve",
"incremental": True,
"plugins": [{"name": "next"}],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}, indent=2)
def create_vercel_json(domain: str) -> str:
return json.dumps({
"rewrites": [],
"headers": [
{
"source": "/(.*)",
"headers": [
{"key": "X-Frame-Options", "value": "DENY"},
{"key": "X-Content-Type-Options", "value": "nosniff"}
]
}
]
}, indent=2)
def create_app_json(name: str, slug: str, domain: str, concept: str) -> str:
return json.dumps({
"name": name,
"slug": slug,
"domain": domain,
"meta": {
"title": f"{name} - {concept}",
"description": f"{name}: {concept}. Join thousands of users.",
"ogImage": "/og.png"
},
"theme": {
"primary": "#6366f1",
"accent": "#f59e0b",
"background": "#0a0a0a",
"font": {
"heading": "Fraunces",
"body": "Space Grotesk"
}
},
"analytics": {
"plausible": domain if domain else None,
"ga": None
},
"header": {
"logo": {
"mark": name[:2].upper(),
"text": name
},
"nav": [
{"label": "Features", "href": "#features"},
{"label": "Pricing", "href": "#pricing"},
{"label": "FAQ", "href": "#faq"}
],
"cta": {"label": "Get Started", "href": "#signup"}
},
"sections": [
{
"type": "hero",
"eyebrow": "Now in beta",
"headline": f"The future of {concept.lower()}",
"subheadline": f"Join thousands of users who are already using {name} to transform their workflow.",
"badges": [],
"primaryCta": {"label": "Request Access", "href": "#signup"},
"secondaryCta": {"label": "Learn More", "href": "#features"},
"image": None
},
{
"type": "stats",
"items": [
{"value": "10K+", "label": "Users"},
{"value": "99.9%", "label": "Uptime"},
{"value": "4.9", "label": "Rating"}
]
},
{
"type": "features",
"title": "Everything you need",
"subtitle": f"Powerful features to supercharge your {concept.lower()}.",
"items": [
{
"icon": "zap",
"title": "Lightning Fast",
"description": "Built for speed from the ground up."
},
{
"icon": "shield",
"title": "Secure by Default",
"description": "Enterprise-grade security out of the box."
},
{
"icon": "trending-up",
"title": "Analytics",
"description": "Deep insights into your performance."
}
]
},
{
"type": "pricing",
"title": "Simple, transparent pricing",
"subtitle": "No hidden fees. Cancel anytime.",
"plans": [
{
"name": "Starter",
"price": {"monthly": 0, "yearly": 0},
"description": "Perfect for getting started",
"features": ["Up to 1,000 requests", "Basic analytics", "Email support"],
"cta": {"label": "Start Free", "href": "#signup"}
},
{
"name": "Pro",
"price": {"monthly": 29, "yearly": 290},
"description": "For growing teams",
"features": ["Unlimited requests", "Advanced analytics", "Priority support", "Custom integrations"],
"cta": {"label": "Get Started", "href": "#signup"},
"highlighted": True
}
]
},
{
"type": "testimonials",
"title": "Loved by teams worldwide",
"items": [
{
"quote": f"{name} has completely transformed how we work. Can't imagine going back.",
"author": "Jane Doe",
"role": "CEO at TechCorp",
"avatar": None
}
]
},
{
"type": "faq",
"title": "Frequently asked questions",
"items": [
{
"q": f"What is {name}?",
"a": f"{name} is a platform for {concept.lower()}. We help teams work faster and smarter."
},
{
"q": "How do I get started?",
"a": "Sign up for a free account and you'll be up and running in minutes."
},
{
"q": "Is there a free trial?",
"a": "Yes! Our Starter plan is free forever. No credit card required."
}
]
},
{
"type": "cta",
"headline": "Ready to get started?",
"subheadline": f"Join thousands of users already using {name}.",
"emailCapture": {
"enabled": True,
"provider": "resend",
"placeholder": "Enter your email",
"buttonText": "Join Waitlist"
}
}
],
"footer": {
"links": [
{"label": "Privacy", "href": "/privacy"},
{"label": "Terms", "href": "/terms"}
],
"social": [
{"platform": "twitter", "href": "#"},
{"platform": "github", "href": "#"}
],
"copyright": f"2024 {name}. All rights reserved."
}
}, indent=2)
def create_layout_tsx(name: str) -> str:
return dedent(f"""\
import type {{ Metadata }} from "next";
import config from "../app.json";
import "./globals.css";
export const metadata: Metadata = {{
title: config.meta.title,
description: config.meta.description,
}};
export default function RootLayout({{
children,
}}: Readonly<{{
children: React.ReactNode;
}}>) {{
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,700&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</head>
<body>{{children}}</body>
</html>
);
}}
""")
def create_page_tsx(ui_package: str) -> str:
return dedent(f"""\
import config from "../app.json";
import {{
Hero,
Stats,
Features,
Pricing,
Testimonials,
FAQ,
CTA,
Header,
Footer,
}} from "{ui_package}";
const sectionComponents: Record<string, React.ComponentType<any>> = {{
hero: Hero,
stats: Stats,
features: Features,
pricing: Pricing,
testimonials: Testimonials,
faq: FAQ,
cta: CTA,
}};
export default function Landing() {{
return (
<main
style={{{{
"--color-primary": config.theme.primary,
"--color-accent": config.theme.accent,
"--color-background": config.theme.background,
}} as React.CSSProperties}}
>
<Header
logo={{config.header.logo}}
nav={{config.header.nav}}
cta={{config.header.cta}}
/>
{{config.sections.map((section, index) => {{
const Component = sectionComponents[section.type];
if (!Component) return null;
return <Component key={{index}} {{...section}} />;
}})}}
<Footer
links={{config.footer.links}}
social={{config.footer.social}}
copyright={{config.footer.copyright}}
/>
</main>
);
}}
""")
def create_globals_css() -> str:
return dedent("""\
@import "tailwindcss";
:root {
--color-primary: #6366f1;
--color-accent: #f59e0b;
--color-background: #0a0a0a;
}
body {
font-family: "Space Grotesk", sans-serif;
background-color: var(--color-background);
color: #ffffff;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Fraunces", serif;
}
""")
def create_gitignore() -> str:
return dedent("""\
# Dependencies
node_modules/
.pnp
.pnp.js
# Build
.next/
out/
build/
# Misc
.DS_Store
*.pem
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Local env
.env*.local
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
""")
def scaffold_landing(
root: Path,
slug: str,
name: str,
domain: str,
concept: str,
ui_package: str,
allow_outside: bool,
) -> None:
"""Create a new landing page project."""
project_dir = root / slug
# Safety check
cwd = Path.cwd()
if not allow_outside and not root.is_relative_to(cwd):
print(f"Error: Target path {root} is outside current directory.")
print("Use --allow-outside to confirm this is intentional.")
sys.exit(1)
if project_dir.exists():
print(f"Error: {project_dir} already exists.")
sys.exit(1)
# Create directories
project_dir.mkdir(parents=True)
(project_dir / "app").mkdir()
(project_dir / "public").mkdir()
# Create files
files = {
"package.json": create_package_json(name, ui_package),
"next.config.ts": create_next_config(),
"tailwind.config.ts": create_tailwind_config(ui_package),
"tsconfig.json": create_tsconfig(),
"vercel.json": create_vercel_json(domain),
"app.json": create_app_json(name, slug, domain, concept),
".gitignore": create_gitignore(),
"app/layout.tsx": create_layout_tsx(name),
"app/page.tsx": create_page_tsx(ui_package),
"app/globals.css": create_globals_css(),
}
for filename, content in files.items():
filepath = project_dir / filename
filepath.parent.mkdir(parents=True, exist_ok=True)
filepath.write_text(content)
print(f"Created: {filepath}")
print(f"\n✅ Landing page created at: {project_dir}")
print(f"\nNext steps:")
print(f"1. cd {project_dir}")
print(f"2. Edit app.json with your content")
print(f"3. Add images to public/")
print(f"4. bun install")
print(f"5. bun dev")
print(f"\nNote: Requires UI package '{ui_package}' to be published.")
def main() -> None:
parser = argparse.ArgumentParser(
description="Scaffold a config-driven NextJS landing page."
)
parser.add_argument(
"--root",
type=Path,
default=Path.cwd(),
help="Parent directory for the landing (default: current directory)",
)
parser.add_argument(
"--slug",
type=str,
required=True,
help="URL-friendly name (e.g., 'mystartup')",
)
parser.add_argument(
"--name",
type=str,
required=True,
help="Display name (e.g., 'My Startup')",
)
parser.add_argument(
"--domain",
type=str,
default="",
help="Domain name (e.g., 'mystartup.com')",
)
parser.add_argument(
"--concept",
type=str,
default="innovative solution",
help="Product concept (e.g., 'AI-powered analytics')",
)
parser.add_argument(
"--ui-package",
type=str,
default=DEFAULT_UI_PACKAGE,
help=f"UI components package (default: {DEFAULT_UI_PACKAGE})",
)
parser.add_argument(
"--allow-outside",
action="store_true",
help="Allow creating files outside current directory",
)
args = parser.parse_args()
scaffold_landing(
root=args.root.resolve(),
slug=args.slug,
name=args.name,
domain=args.domain,
concept=args.concept,
ui_package=args.ui_package,
allow_outside=args.allow_outside,
)
if __name__ == "__main__":
main()