Build a SaaS Landing Page with AI Coding: Complete Walkthrough
Step-by-step guide to building a production-ready SaaS landing page with Astro, Tailwind, and AI. Includes every prompt, code snippet, and deployment to Vercel.
A beautiful SaaS landing page in under 2 hours. No design skills required.
We’re building the full thing: hero section, feature grid, pricing table, testimonials, waitlist form with email collection, and deployment to Vercel. Every section built with AI prompts, every decision explained.
| What You’ll Build | |
|---|---|
| Project | Landing page for “Clipflow” — a fictional AI clipboard manager |
| Tech | Astro + Tailwind CSS |
| Sections | Hero, Features, Pricing, Testimonials, Waitlist CTA, Footer |
| Deployment | Vercel (free tier) |
| Time | ~2 hours |
| Difficulty | ⭐⭐ Intermediate |
Haven’t used AI for coding before? Read Prompts That Actually Work first — it’ll make every prompt in this guide more effective.
Step 1: Project Setup
The Prompt:
Create a new Astro project for a SaaS landing page. I want:
- Astro with Tailwind CSS integration
- A single-page layout (all sections on one page)
- Clean project structure with components for each section
- Dark mode design system
Give me the terminal commands to set up the project, then the base layout file.
Commands:
npm create astro@latest clipflow-landing -- --template minimal
cd clipflow-landing
npx astro add tailwind
Base layout — src/layouts/Layout.astro:
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Clipflow — AI-powered clipboard manager that organizes everything you copy.">
<title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-gray-950 text-gray-100 font-['Inter']">
<slot />
</body>
</html>
Main page — src/pages/index.astro:
---
import Layout from '../layouts/Layout.astro';
import Nav from '../components/Nav.astro';
import Hero from '../components/Hero.astro';
import Features from '../components/Features.astro';
import Pricing from '../components/Pricing.astro';
import Testimonials from '../components/Testimonials.astro';
import Waitlist from '../components/Waitlist.astro';
import Footer from '../components/Footer.astro';
---
<Layout title="Clipflow — AI Clipboard Manager">
<Nav />
<Hero />
<Features />
<Pricing />
<Testimonials />
<Waitlist />
<Footer />
</Layout>
Clean. One component per section. Easy to iterate on each independently.
Step 2: Navigation
The Prompt:
Create a Nav.astro component for the Clipflow landing page. It should:
- Sticky at top, transparent background that becomes solid on scroll
- Logo text “Clipflow” on the left
- Links: Features, Pricing, Testimonials (anchor links)
- CTA button “Join Waitlist” on the right
- Mobile hamburger menu
- Use Tailwind CSS classes only
---
// Nav.astro
---
<nav id="nav" class="fixed top-0 w-full z-50 transition-all duration-300">
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" class="text-xl font-bold text-white">
✦ Clipflow
</a>
<div class="hidden md:flex items-center gap-8">
<a href="#features" class="text-gray-400 hover:text-white transition">Features</a>
<a href="#pricing" class="text-gray-400 hover:text-white transition">Pricing</a>
<a href="#testimonials" class="text-gray-400 hover:text-white transition">Testimonials</a>
<a href="#waitlist" class="bg-violet-600 hover:bg-violet-500 text-white px-5 py-2 rounded-lg font-medium transition">
Join Waitlist
</a>
</div>
<button id="menu-toggle" class="md:hidden text-gray-400">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
</div>
<div id="mobile-menu" class="hidden md:hidden bg-gray-900 border-t border-gray-800">
<div class="px-6 py-4 flex flex-col gap-4">
<a href="#features" class="text-gray-400 hover:text-white">Features</a>
<a href="#pricing" class="text-gray-400 hover:text-white">Pricing</a>
<a href="#testimonials" class="text-gray-400 hover:text-white">Testimonials</a>
<a href="#waitlist" class="bg-violet-600 text-white px-5 py-2 rounded-lg text-center font-medium">
Join Waitlist
</a>
</div>
</div>
</nav>
<script>
// Scroll effect
window.addEventListener('scroll', () => {
const nav = document.getElementById('nav');
if (window.scrollY > 50) {
nav.classList.add('bg-gray-950/90', 'backdrop-blur-lg', 'border-b', 'border-gray-800');
} else {
nav.classList.remove('bg-gray-950/90', 'backdrop-blur-lg', 'border-b', 'border-gray-800');
}
});
// Mobile toggle
document.getElementById('menu-toggle')?.addEventListener('click', () => {
document.getElementById('mobile-menu')?.classList.toggle('hidden');
});
</script>
Step 3: Hero Section
The hero is the most important section. It needs to communicate what Clipflow does in 3 seconds.
The Prompt:
Create a Hero.astro component for “Clipflow — AI-powered clipboard manager.” The hero should:
- Large heading with gradient text
- Subtitle: “Everything you copy, organized automatically. Clipflow uses AI to categorize, search, and sync your clipboard across devices.”
- Two CTA buttons: “Join the Waitlist” (primary) and “See How It Works” (secondary/outline)
- A mockup area below (a styled div that looks like an app window with sample clipboard entries)
- Subtle gradient background orbs/blobs for visual interest
- Generous whitespace, centered text
- Tailwind only
---
// Hero.astro
---
<section class="relative min-h-screen flex items-center justify-center overflow-hidden pt-20">
<!-- Background blobs -->
<div class="absolute top-1/4 -left-32 w-96 h-96 bg-violet-600/20 rounded-full blur-3xl"></div>
<div class="absolute bottom-1/4 -right-32 w-96 h-96 bg-blue-600/20 rounded-full blur-3xl"></div>
<div class="relative max-w-4xl mx-auto px-6 text-center">
<div class="inline-block px-4 py-1 rounded-full bg-violet-600/10 border border-violet-500/20 text-violet-400 text-sm font-medium mb-6">
✨ Now in private beta
</div>
<h1 class="text-5xl md:text-7xl font-bold leading-tight mb-6">
Your clipboard,
<span class="bg-gradient-to-r from-violet-400 to-blue-400 bg-clip-text text-transparent">
supercharged
</span>
</h1>
<p class="text-xl text-gray-400 max-w-2xl mx-auto mb-10">
Everything you copy, organized automatically. Clipflow uses AI to categorize,
search, and sync your clipboard across all your devices.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center mb-16">
<a href="#waitlist" class="bg-violet-600 hover:bg-violet-500 text-white px-8 py-3 rounded-lg font-semibold text-lg transition">
Join the Waitlist
</a>
<a href="#features" class="border border-gray-700 hover:border-gray-500 text-gray-300 px-8 py-3 rounded-lg font-semibold text-lg transition">
See How It Works
</a>
</div>
<!-- App mockup -->
<div class="bg-gray-900 border border-gray-800 rounded-xl shadow-2xl overflow-hidden max-w-2xl mx-auto">
<div class="flex items-center gap-2 px-4 py-3 bg-gray-900 border-b border-gray-800">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span class="ml-2 text-sm text-gray-500">Clipflow</span>
</div>
<div class="p-4 space-y-3">
<div class="flex items-center gap-3 p-3 bg-gray-800/50 rounded-lg">
<span class="text-violet-400">📋</span>
<div>
<p class="text-sm text-white">API endpoint: https://api.example.com/v2/users</p>
<p class="text-xs text-gray-500">Code snippet · 2 min ago</p>
</div>
</div>
<div class="flex items-center gap-3 p-3 bg-gray-800/50 rounded-lg">
<span class="text-blue-400">🔗</span>
<div>
<p class="text-sm text-white">Meeting notes from Q1 planning session</p>
<p class="text-xs text-gray-500">Text · 15 min ago</p>
</div>
</div>
<div class="flex items-center gap-3 p-3 bg-gray-800/50 rounded-lg">
<span class="text-green-400">🖼️</span>
<div>
<p class="text-sm text-white">Screenshot — dashboard-redesign-v3.png</p>
<p class="text-xs text-gray-500">Image · 1 hour ago</p>
</div>
</div>
</div>
</div>
</div>
</section>
Why this works: Gradient text for the hook, social proof badge (“private beta” creates urgency), two CTAs for different intent levels, and a fake app mockup that sells the product visually without needing actual screenshots.
Step 4: Features Grid
The Prompt:
Create a Features.astro component with 6 features in a responsive grid (3 columns on desktop, 1 on mobile). Each feature has an emoji icon, title, and description. Features for an AI clipboard manager: smart categorization, cross-device sync, instant search, code snippet detection, privacy first, team sharing. Use Tailwind. Add a section header “Built for how you actually work.”
---
const features = [
{ icon: "🧠", title: "Smart Categorization", desc: "AI automatically sorts clips into categories — code, links, text, images — so you never have to." },
{ icon: "🔄", title: "Cross-Device Sync", desc: "Copy on your phone, paste on your laptop. Encrypted end-to-end, synced in real-time." },
{ icon: "⚡", title: "Instant Search", desc: "Find anything you've ever copied. Full-text search across your entire clipboard history." },
{ icon: "💻", title: "Code Detection", desc: "Automatically detects code snippets, applies syntax highlighting, and identifies the language." },
{ icon: "🔒", title: "Privacy First", desc: "Your data never leaves your devices unencrypted. We can't read your clips even if we wanted to." },
{ icon: "👥", title: "Team Sharing", desc: "Share clips with your team. Create shared collections for projects, onboarding, and more." }
];
---
<section id="features" class="py-24 px-6">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold mb-4">Built for how you actually work</h2>
<p class="text-gray-400 text-lg max-w-2xl mx-auto">
Not another clipboard manager that gets in the way. Clipflow works silently in the background until you need it.
</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map(f => (
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6 hover:border-violet-500/50 transition-all duration-300 hover:-translate-y-1">
<div class="text-3xl mb-4">{f.icon}</div>
<h3 class="text-lg font-semibold mb-2">{f.title}</h3>
<p class="text-gray-400 text-sm leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
</section>
Step 5: Pricing Section
The Prompt:
Create a Pricing.astro component with 3 tiers: Free, Pro ($8/mo), and Team ($16/user/mo). Each tier has a feature list with checkmarks. The Pro tier should be highlighted as “Most Popular” with a violet border and badge. Include a toggle for monthly/yearly pricing (yearly = 20% off). Tailwind only.
---
const tiers = [
{
name: "Free",
monthly: 0,
yearly: 0,
desc: "For personal use",
features: ["500 clip history", "Basic search", "1 device", "Manual categories"],
highlighted: false
},
{
name: "Pro",
monthly: 8,
yearly: 77,
desc: "For power users",
features: ["Unlimited history", "AI categorization", "5 devices", "Code detection", "Priority support"],
highlighted: true
},
{
name: "Team",
monthly: 16,
yearly: 154,
desc: "Per user, for teams",
features: ["Everything in Pro", "Shared collections", "Admin dashboard", "SSO", "API access", "Dedicated support"],
highlighted: false
}
];
---
<section id="pricing" class="py-24 px-6">
<div class="max-w-5xl mx-auto">
<div class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold mb-4">Simple, honest pricing</h2>
<p class="text-gray-400 text-lg mb-8">Start free. Upgrade when you need more.</p>
<div class="inline-flex bg-gray-900 rounded-lg p-1" id="billing-toggle">
<button class="px-4 py-2 rounded-md text-sm font-medium bg-violet-600 text-white" data-period="monthly">Monthly</button>
<button class="px-4 py-2 rounded-md text-sm font-medium text-gray-400" data-period="yearly">Yearly (save 20%)</button>
</div>
</div>
<div class="grid md:grid-cols-3 gap-6">
{tiers.map(tier => (
<div class={`rounded-xl p-6 ${tier.highlighted
? 'bg-gray-900 border-2 border-violet-500 relative'
: 'bg-gray-900 border border-gray-800'}`}>
{tier.highlighted && (
<div class="absolute -top-3 left-1/2 -translate-x-1/2 bg-violet-600 text-white text-xs font-bold px-3 py-1 rounded-full">
Most Popular
</div>
)}
<h3 class="text-xl font-bold mb-1">{tier.name}</h3>
<p class="text-gray-500 text-sm mb-4">{tier.desc}</p>
<div class="mb-6">
<span class="text-4xl font-bold price-monthly">${tier.monthly}</span>
<span class="text-4xl font-bold price-yearly hidden">${tier.yearly}</span>
<span class="text-gray-500">{tier.monthly > 0 ? '/mo' : ''}</span>
</div>
<ul class="space-y-3 mb-8">
{tier.features.map(f => (
<li class="flex items-center gap-2 text-sm text-gray-300">
<span class="text-violet-400">✓</span> {f}
</li>
))}
</ul>
<a href="#waitlist" class={`block text-center py-3 rounded-lg font-medium transition ${
tier.highlighted
? 'bg-violet-600 hover:bg-violet-500 text-white'
: 'bg-gray-800 hover:bg-gray-700 text-gray-300'
}`}>
{tier.monthly === 0 ? 'Get Started' : 'Join Waitlist'}
</a>
</div>
))}
</div>
</div>
</section>
<script>
const toggle = document.getElementById('billing-toggle');
toggle?.addEventListener('click', (e) => {
const btn = (e.target as HTMLElement).closest('button');
if (!btn) return;
const period = btn.dataset.period;
toggle.querySelectorAll('button').forEach(b => {
b.classList.remove('bg-violet-600', 'text-white');
b.classList.add('text-gray-400');
});
btn.classList.add('bg-violet-600', 'text-white');
btn.classList.remove('text-gray-400');
document.querySelectorAll('.price-monthly').forEach(el => {
el.classList.toggle('hidden', period === 'yearly');
});
document.querySelectorAll('.price-yearly').forEach(el => {
el.classList.toggle('hidden', period === 'monthly');
});
});
</script>
Step 6: Testimonials
The Prompt:
Create a Testimonials.astro component with 3 testimonial cards. Fictional but realistic quotes from a developer, a designer, and a product manager about using the clipboard manager. Each has name, role, company, and quote. Tailwind dark theme.
---
const testimonials = [
{
quote: "I used to lose code snippets constantly. Now I just search in Clipflow and everything's there. It's like having a second brain for my clipboard.",
name: "Sarah Chen",
role: "Senior Developer",
company: "Stripe"
},
{
quote: "The AI categorization is scary good. It knows the difference between a hex color, a URL, and a paragraph of text without me doing anything.",
name: "Marcus Rivera",
role: "Product Designer",
company: "Figma"
},
{
quote: "We use shared collections for our onboarding docs. New hires get access to every important link and snippet on day one.",
name: "Priya Sharma",
role: "Product Manager",
company: "Notion"
}
];
---
<section id="testimonials" class="py-24 px-6 bg-gray-900/50">
<div class="max-w-6xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold mb-4">Loved by people who copy things</h2>
<p class="text-gray-400 text-lg">That's basically everyone.</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
{testimonials.map(t => (
<div class="bg-gray-900 border border-gray-800 rounded-xl p-6">
<p class="text-gray-300 text-sm leading-relaxed mb-6">"{t.quote}"</p>
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-blue-500 flex items-center justify-center text-white font-bold text-sm">
{t.name.charAt(0)}
</div>
<div>
<p class="text-white font-medium text-sm">{t.name}</p>
<p class="text-gray-500 text-xs">{t.role} at {t.company}</p>
</div>
</div>
</div>
))}
</div>
</div>
</section>
Step 7: Waitlist Form (Email Collection)
This is the most important conversion section. We need a form that actually works.
The Prompt:
Create a Waitlist.astro component with an email signup form. It should:
- Centered section with compelling copy
- Email input + submit button on one line
- Form submits to a serverless function or Formspree endpoint
- Client-side validation
- Success/error states
- “No spam” trust signal
- Tailwind dark theme
---
// Waitlist.astro — uses Formspree for email collection (free tier: 50 submissions/month)
---
<section id="waitlist" class="py-24 px-6">
<div class="max-w-2xl mx-auto text-center">
<h2 class="text-3xl md:text-4xl font-bold mb-4">Get early access</h2>
<p class="text-gray-400 text-lg mb-8">
Join 2,000+ people waiting for Clipflow. Early access members get Pro free for 6 months.
</p>
<form id="waitlist-form" action="https://formspree.io/f/YOUR_FORM_ID" method="POST"
class="flex flex-col sm:flex-row gap-3 max-w-md mx-auto mb-4">
<input
type="email"
name="email"
required
placeholder="[email protected]"
class="flex-1 px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-violet-500 transition"
/>
<button type="submit"
class="bg-violet-600 hover:bg-violet-500 text-white px-6 py-3 rounded-lg font-semibold transition whitespace-nowrap">
Join Waitlist
</button>
</form>
<p class="text-gray-600 text-xs">No spam. Unsubscribe anytime. We'll only email about launch updates.</p>
<div id="form-success" class="hidden mt-4 p-4 bg-green-900/30 border border-green-700 rounded-lg text-green-400">
🎉 You're on the list! Check your email for confirmation.
</div>
<div id="form-error" class="hidden mt-4 p-4 bg-red-900/30 border border-red-700 rounded-lg text-red-400">
Something went wrong. Please try again.
</div>
</div>
</section>
<script>
const form = document.getElementById('waitlist-form') as HTMLFormElement;
form?.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = form.querySelector('button') as HTMLButtonElement;
btn.textContent = 'Joining...';
btn.disabled = true;
try {
const res = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { 'Accept': 'application/json' }
});
if (res.ok) {
form.classList.add('hidden');
document.getElementById('form-success')?.classList.remove('hidden');
} else {
throw new Error('Submit failed');
}
} catch {
document.getElementById('form-error')?.classList.remove('hidden');
btn.textContent = 'Join Waitlist';
btn.disabled = false;
}
});
</script>
Setting up Formspree:
- Go to formspree.io and create a free account
- Create a new form
- Replace
YOUR_FORM_IDwith your actual form ID - Done — emails go to your Formspree dashboard
For something more robust, use Resend or a Supabase table. But Formspree is perfect for a landing page launch.
Step 8: Footer
The Prompt:
Create a simple Footer.astro component. Logo, copyright, and links to Twitter, GitHub, and a privacy policy. Tailwind dark theme, minimal.
<footer class="border-t border-gray-800 py-8 px-6">
<div class="max-w-6xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<p class="text-gray-500 text-sm">© 2026 Clipflow. All rights reserved.</p>
<div class="flex gap-6">
<a href="#" class="text-gray-500 hover:text-white text-sm transition">Twitter</a>
<a href="#" class="text-gray-500 hover:text-white text-sm transition">GitHub</a>
<a href="#" class="text-gray-500 hover:text-white text-sm transition">Privacy</a>
</div>
</div>
</footer>
What Went Wrong: Debugging
Problem 1: Tailwind Classes Not Applying
After setup, you see unstyled HTML. This is the most common Astro + Tailwind issue.
Fix: Check astro.config.mjs has the Tailwind integration, and tailwind.config.mjs includes your content paths:
// tailwind.config.mjs
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
theme: { extend: {} },
plugins: [],
}
Problem 2: Formspree Returns 403
If the form submission fails, it’s usually because:
- You’re testing on
localhostand haven’t verified your Formspree email - The
Accept: application/jsonheader is missing (form redirects to Formspree’s thank-you page instead)
Problem 3: Mobile Menu Doesn’t Close on Link Click
The hamburger menu opens but doesn’t close when you click a link. Add this to the Nav script:
document.querySelectorAll('#mobile-menu a').forEach(link => {
link.addEventListener('click', () => {
document.getElementById('mobile-menu')?.classList.add('hidden');
});
});
Step 9: Deploy to Vercel
The Prompt:
Give me the steps to deploy an Astro static site to Vercel, including the vercel.json config if needed.
It’s simpler than you think:
# Install Vercel CLI
npm i -g vercel
# Deploy (from project root)
vercel
# Follow the prompts:
# - Link to existing project? No
# - Project name: clipflow-landing
# - Framework: Astro
# - Build command: (auto-detected)
# - Output directory: (auto-detected)
That’s it. Vercel auto-detects Astro and configures everything. You’ll get a URL like clipflow-landing.vercel.app.
For a custom domain:
- Go to Vercel dashboard → your project → Settings → Domains
- Add your domain
- Update DNS records as instructed
Performance Checklist
Before sharing your URL, run through this:
- Lighthouse score > 90 on all metrics (Astro makes this easy)
- Mobile responsive at 375px width
- All anchor links scroll smoothly
- Waitlist form submits correctly
- Meta description and title set for SEO
- Favicon added
- Open Graph image for social sharing
Next Steps
You have a production landing page. Here’s what to do next:
- Add analytics — Plausible or Fathom for privacy-friendly tracking
- Add a blog — Astro has built-in content collections (that’s what we use at VibeWerks)
- A/B test the hero — try different headlines and CTAs
- Set up email sequences — connect Formspree to Mailchimp or Resend for drip campaigns
Check out our other guides:
- Build Your First AI App in 30 Minutes — go deeper with a full app
- 10 Perfect First Projects — more ideas to build
- 7 Mistakes Every New Vibecoder Makes — don’t trip on these
The landing page is the easiest part of a SaaS. The hard part is getting people to visit it. But at least now you know you can build one in an afternoon with AI and Astro. Ship it, share it, iterate. 🚀