indie-landing-page/SKILL.md
Build high-converting landing pages for indie iOS/macOS apps. Covers Next.js setup, App Store optimization, email capture, SEO, analytics, and conversion optimization.
npx skillsauth add abanoub-ashraf/manus-skills-import indie-landing-pageInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build high-converting landing pages for indie iOS/macOS apps. Covers Next.js setup, App Store optimization, email capture, SEO, analytics, and conversion optimization.
Framework: Next.js 14+ (App Router)
Styling: Tailwind CSS
Deployment: Vercel (free tier works)
Email: Resend, ConvertKit, or Buttondown
Analytics: Plausible, Fathom, or Vercel Analytics
CMS (optional): MDX for blog/changelog
Why Next.js?
app/
├── layout.tsx # Root layout with metadata
├── page.tsx # Homepage / main landing
├── press/
│ └── page.tsx # Press kit
├── privacy/
│ └── page.tsx # Privacy policy
├── terms/
│ └── page.tsx # Terms of service
├── changelog/
│ └── page.tsx # App changelog
└── blog/
└── [slug]/
└── page.tsx # Blog posts (SEO)
components/
├── Hero.tsx # Above the fold
├── Features.tsx # Feature showcase
├── Screenshots.tsx # App screenshots
├── Testimonials.tsx # Social proof
├── Pricing.tsx # Pricing section
├── FAQ.tsx # FAQs
├── CTA.tsx # Call to action
├── AppStoreButton.tsx # Download buttons
├── EmailCapture.tsx # Waitlist form
└── SmartBanner.tsx # iOS smart banner
lib/
├── metadata.ts # SEO helpers
└── analytics.ts # Event tracking
// app/layout.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://yourapp.com'),
title: {
default: 'AppName - Tagline describing key benefit',
template: '%s | AppName'
},
description: 'Primary app description for search results. Include key benefit and call to action. 150-160 chars optimal.',
keywords: ['keyword1', 'keyword2', 'ios app', 'iphone app'],
authors: [{ name: 'Your Name' }],
creator: 'Your Name',
publisher: 'Your Company',
// Open Graph
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://yourapp.com',
siteName: 'AppName',
title: 'AppName - Tagline',
description: 'Description for social sharing',
images: [
{
url: '/og-image.png', // 1200x630
width: 1200,
height: 630,
alt: 'AppName Preview',
},
],
},
// Twitter
twitter: {
card: 'summary_large_image',
title: 'AppName - Tagline',
description: 'Description for Twitter',
creator: '@yourhandle',
images: ['/og-image.png'],
},
// App-specific
appleWebApp: {
capable: true,
title: 'AppName',
statusBarStyle: 'black-translucent',
},
// Verification
verification: {
google: 'your-google-verification-code',
},
// Robots
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<head>
{/* Smart App Banner */}
<meta name="apple-itunes-app" content="app-id=YOUR_APP_ID, app-argument=https://yourapp.com" />
{/* Favicon */}
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
</head>
<body>{children}</body>
</html>
)
}
The smart banner shows a native iOS banner prompting users to download your app.
// In <head>
<meta
name="apple-itunes-app"
content="app-id=123456789, app-argument=yourapp://deeplink"
/>
Parameters:
app-id: Your App Store ID (from App Store Connect URL)app-argument: Deep link URL passed to app if installedaffiliate-data: Affiliate token (optional)// components/Hero.tsx
import Image from 'next/image'
import { AppStoreButton } from './AppStoreButton'
export function Hero() {
return (
<section className="relative overflow-hidden bg-gradient-to-b from-blue-50 to-white py-20 sm:py-32">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="lg:grid lg:grid-cols-12 lg:gap-x-8 lg:gap-y-20">
{/* Copy */}
<div className="relative z-10 mx-auto max-w-2xl lg:col-span-7 lg:max-w-none lg:pt-6 xl:col-span-6">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Primary benefit in one clear statement
</h1>
<p className="mt-6 text-lg text-gray-600">
Expand on the benefit. Explain what the app does and why it matters.
Keep it to 2-3 sentences max.
</p>
{/* CTA */}
<div className="mt-8 flex flex-wrap gap-4">
<AppStoreButton />
<a
href="#features"
className="inline-flex items-center rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
>
Learn more →
</a>
</div>
{/* Social Proof */}
<div className="mt-8 flex items-center gap-x-6">
<div className="flex -space-x-2">
{/* User avatars */}
</div>
<div className="text-sm text-gray-600">
<span className="font-semibold text-gray-900">10,000+</span> happy users
</div>
<div className="flex items-center text-sm text-gray-600">
<span className="text-yellow-400">★★★★★</span>
<span className="ml-1">4.9 on App Store</span>
</div>
</div>
</div>
{/* App Screenshot */}
<div className="relative mt-10 sm:mt-20 lg:col-span-5 lg:row-span-2 lg:mt-0 xl:col-span-6">
<div className="relative mx-auto w-[366px]">
{/* Phone frame */}
<Image
src="/iphone-frame.png"
alt=""
width={366}
height={748}
className="relative z-10"
/>
{/* Screenshot */}
<Image
src="/screenshot-hero.png"
alt="App screenshot"
width={318}
height={690}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-[32px]"
/>
</div>
</div>
</div>
</div>
</section>
)
}
// components/AppStoreButton.tsx
import Image from 'next/image'
interface AppStoreButtonProps {
className?: string
}
export function AppStoreButton({ className = '' }: AppStoreButtonProps) {
return (
<a
href="https://apps.apple.com/app/idYOUR_APP_ID"
target="_blank"
rel="noopener noreferrer"
className={`inline-block ${className}`}
>
<Image
src="/app-store-badge.svg"
alt="Download on the App Store"
width={156}
height={52}
className="h-[52px] w-auto"
/>
</a>
)
}
Get official badge: https://developer.apple.com/app-store/marketing/guidelines/
// components/EmailCapture.tsx
'use client'
import { useState } from 'react'
export function EmailCapture() {
const [email, setEmail] = useState('')
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setStatus('loading')
try {
const res = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
if (!res.ok) throw new Error()
setStatus('success')
setEmail('')
} catch {
setStatus('error')
}
}
return (
<div className="mx-auto max-w-md">
<h3 className="text-lg font-semibold text-gray-900">
Get notified when we launch
</h3>
<p className="mt-2 text-sm text-gray-600">
Join 1,000+ people waiting for the app.
</p>
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
required
className="flex-1 rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
<button
type="submit"
disabled={status === 'loading'}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{status === 'loading' ? 'Joining...' : 'Join Waitlist'}
</button>
</form>
{status === 'success' && (
<p className="mt-2 text-sm text-green-600">
You're on the list! We'll be in touch soon.
</p>
)}
{status === 'error' && (
<p className="mt-2 text-sm text-red-600">
Something went wrong. Please try again.
</p>
)}
</div>
)
}
// app/api/subscribe/route.ts
import { Resend } from 'resend'
import { NextResponse } from 'next/server'
const resend = new Resend(process.env.RESEND_API_KEY)
export async function POST(req: Request) {
try {
const { email } = await req.json()
await resend.contacts.create({
email,
audienceId: process.env.RESEND_AUDIENCE_ID!,
})
return NextResponse.json({ success: true })
} catch (error) {
return NextResponse.json({ error: 'Failed to subscribe' }, { status: 500 })
}
}
// components/Features.tsx
import {
BoltIcon,
ShieldCheckIcon,
DevicePhoneMobileIcon
} from '@heroicons/react/24/outline'
const features = [
{
name: 'Feature One',
description: 'Brief description of what this feature does and why it benefits the user.',
icon: BoltIcon,
},
{
name: 'Feature Two',
description: 'Brief description of what this feature does and why it benefits the user.',
icon: ShieldCheckIcon,
},
{
name: 'Feature Three',
description: 'Brief description of what this feature does and why it benefits the user.',
icon: DevicePhoneMobileIcon,
},
]
export function Features() {
return (
<section id="features" className="py-20 sm:py-32">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Everything you need to [achieve outcome]
</h2>
<p className="mt-4 text-lg text-gray-600">
Supporting text that expands on the headline.
</p>
</div>
<div className="mx-auto mt-16 max-w-5xl">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
{features.map((feature) => (
<div key={feature.name} className="relative rounded-2xl border border-gray-200 p-8">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-600">
<feature.icon className="h-6 w-6 text-white" />
</div>
<h3 className="mt-4 text-lg font-semibold text-gray-900">
{feature.name}
</h3>
<p className="mt-2 text-gray-600">
{feature.description}
</p>
</div>
))}
</div>
</div>
</div>
</section>
)
}
// components/Testimonials.tsx
const testimonials = [
{
content: "This app completely changed how I [do thing]. Can't imagine going back.",
author: 'Sarah Johnson',
role: 'Product Designer',
avatar: '/testimonials/sarah.jpg',
},
{
content: "Finally an app that just works. Simple, beautiful, and effective.",
author: 'Mike Chen',
role: 'Software Engineer',
avatar: '/testimonials/mike.jpg',
},
{
content: "I've tried every app in this category. This is the one I stuck with.",
author: 'Emily Davis',
role: 'Entrepreneur',
avatar: '/testimonials/emily.jpg',
},
]
export function Testimonials() {
return (
<section className="bg-gray-50 py-20 sm:py-32">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h2 className="text-center text-3xl font-bold tracking-tight text-gray-900">
Loved by thousands
</h2>
<div className="mx-auto mt-16 grid max-w-5xl grid-cols-1 gap-8 lg:grid-cols-3">
{testimonials.map((testimonial, i) => (
<figure key={i} className="rounded-2xl bg-white p-8 shadow-sm">
<blockquote className="text-gray-700">
"{testimonial.content}"
</blockquote>
<figcaption className="mt-6 flex items-center gap-x-4">
<img
src={testimonial.avatar}
alt=""
className="h-10 w-10 rounded-full bg-gray-100"
/>
<div>
<div className="font-semibold text-gray-900">{testimonial.author}</div>
<div className="text-sm text-gray-600">{testimonial.role}</div>
</div>
</figcaption>
</figure>
))}
</div>
</div>
</section>
)
}
// components/Pricing.tsx
import { CheckIcon } from '@heroicons/react/24/solid'
const tiers = [
{
name: 'Free',
price: '$0',
description: 'Perfect for getting started',
features: ['Feature 1', 'Feature 2', 'Feature 3'],
cta: 'Download Free',
highlighted: false,
},
{
name: 'Pro',
price: '$4.99',
period: '/month',
description: 'For power users',
features: ['Everything in Free', 'Feature 4', 'Feature 5', 'Feature 6'],
cta: 'Start Free Trial',
highlighted: true,
},
]
export function Pricing() {
return (
<section id="pricing" className="py-20 sm:py-32">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
Simple, transparent pricing
</h2>
<p className="mt-4 text-lg text-gray-600">
Start free, upgrade when you need more.
</p>
</div>
<div className="mx-auto mt-16 grid max-w-lg grid-cols-1 gap-8 lg:max-w-4xl lg:grid-cols-2">
{tiers.map((tier) => (
<div
key={tier.name}
className={`rounded-2xl p-8 ${
tier.highlighted
? 'bg-blue-600 text-white ring-2 ring-blue-600'
: 'bg-white ring-1 ring-gray-200'
}`}
>
<h3 className={`text-lg font-semibold ${tier.highlighted ? 'text-white' : 'text-gray-900'}`}>
{tier.name}
</h3>
<p className={`mt-4 flex items-baseline ${tier.highlighted ? 'text-white' : 'text-gray-900'}`}>
<span className="text-4xl font-bold">{tier.price}</span>
{tier.period && <span className="ml-1 text-sm">{tier.period}</span>}
</p>
<p className={`mt-2 text-sm ${tier.highlighted ? 'text-blue-100' : 'text-gray-600'}`}>
{tier.description}
</p>
<ul className="mt-8 space-y-3">
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-x-3 text-sm">
<CheckIcon className={`h-5 w-5 flex-shrink-0 ${tier.highlighted ? 'text-white' : 'text-blue-600'}`} />
{feature}
</li>
))}
</ul>
<a
href="https://apps.apple.com/app/idYOUR_APP_ID"
className={`mt-8 block rounded-lg px-4 py-2.5 text-center text-sm font-semibold ${
tier.highlighted
? 'bg-white text-blue-600 hover:bg-blue-50'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{tier.cta}
</a>
</div>
))}
</div>
</div>
</section>
)
}
// components/FAQ.tsx
'use client'
import { useState } from 'react'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
const faqs = [
{
question: 'Is there a free version?',
answer: 'Yes! The core features are completely free. Pro unlocks advanced features.',
},
{
question: 'Does it work offline?',
answer: 'Yes, all your data is stored locally and syncs when you\'re back online.',
},
{
question: 'Can I cancel anytime?',
answer: 'Absolutely. Cancel your subscription anytime from your App Store settings.',
},
{
question: 'Is my data private?',
answer: 'Your data never leaves your device unless you enable sync. We can\'t see it.',
},
]
export function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(null)
return (
<section id="faq" className="py-20 sm:py-32">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<h2 className="text-center text-3xl font-bold tracking-tight text-gray-900">
Frequently asked questions
</h2>
<dl className="mt-12 space-y-4">
{faqs.map((faq, i) => (
<div key={i} className="rounded-lg border border-gray-200">
<dt>
<button
onClick={() => setOpenIndex(openIndex === i ? null : i)}
className="flex w-full items-center justify-between px-6 py-4 text-left"
>
<span className="font-medium text-gray-900">{faq.question}</span>
<ChevronDownIcon
className={`h-5 w-5 text-gray-500 transition-transform ${
openIndex === i ? 'rotate-180' : ''
}`}
/>
</button>
</dt>
{openIndex === i && (
<dd className="px-6 pb-4 text-gray-600">
{faq.answer}
</dd>
)}
</div>
))}
</dl>
</div>
</section>
)
}
// app/press/page.tsx
import { Metadata } from 'next'
import Image from 'next/image'
export const metadata: Metadata = {
title: 'Press Kit',
description: 'Download logos, screenshots, and press materials for AppName.',
}
export default function PressPage() {
return (
<main className="py-20">
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8">
<h1 className="text-4xl font-bold text-gray-900">Press Kit</h1>
<p className="mt-4 text-lg text-gray-600">
Everything you need to write about AppName.
</p>
{/* About */}
<section className="mt-12">
<h2 className="text-2xl font-bold text-gray-900">About AppName</h2>
<p className="mt-4 text-gray-600">
[2-3 paragraph description of your app, its mission, and key features.
Make it easy for journalists to copy-paste.]
</p>
</section>
{/* Quick Facts */}
<section className="mt-12">
<h2 className="text-2xl font-bold text-gray-900">Quick Facts</h2>
<dl className="mt-4 grid grid-cols-2 gap-4">
<div>
<dt className="font-medium text-gray-900">Launch Date</dt>
<dd className="text-gray-600">January 2025</dd>
</div>
<div>
<dt className="font-medium text-gray-900">Platform</dt>
<dd className="text-gray-600">iOS, iPadOS</dd>
</div>
<div>
<dt className="font-medium text-gray-900">Price</dt>
<dd className="text-gray-600">Free with Pro subscription</dd>
</div>
<div>
<dt className="font-medium text-gray-900">Users</dt>
<dd className="text-gray-600">10,000+</dd>
</div>
</dl>
</section>
{/* Assets */}
<section className="mt-12">
<h2 className="text-2xl font-bold text-gray-900">Brand Assets</h2>
<div className="mt-4 space-y-4">
<a href="/press/logo-kit.zip" className="block rounded-lg border border-gray-200 p-4 hover:bg-gray-50">
<div className="font-medium text-gray-900">Logo Kit</div>
<div className="text-sm text-gray-600">PNG, SVG, and PDF formats</div>
</a>
<a href="/press/screenshots.zip" className="block rounded-lg border border-gray-200 p-4 hover:bg-gray-50">
<div className="font-medium text-gray-900">Screenshots</div>
<div className="text-sm text-gray-600">High-resolution app screenshots</div>
</a>
<a href="/press/icon.zip" className="block rounded-lg border border-gray-200 p-4 hover:bg-gray-50">
<div className="font-medium text-gray-900">App Icon</div>
<div className="text-sm text-gray-600">All sizes, with and without background</div>
</a>
</div>
</section>
{/* Contact */}
<section className="mt-12">
<h2 className="text-2xl font-bold text-gray-900">Press Contact</h2>
<p className="mt-4 text-gray-600">
For press inquiries, contact{' '}
<a href="mailto:[email protected]" className="text-blue-600 hover:underline">
[email protected]
</a>
</p>
</section>
</div>
</main>
)
}
□ Canonical URLs set
□ Sitemap.xml generated (Next.js automatic)
□ Robots.txt configured
□ HTTPS enabled (Vercel default)
□ Fast load times (<3s)
□ Mobile responsive
□ Core Web Vitals passing
□ Title tag optimized (50-60 chars)
□ Meta description written (150-160 chars)
□ H1 contains primary keyword
□ Image alt text on all images
□ Internal linking structure
□ Schema markup for app
□ App Store link with campaign tracking
□ Smart app banner configured
□ Deep linking setup
□ App name in title tag
□ "Download" or "Get" keywords included
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://yourapp.com',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1,
},
{
url: 'https://yourapp.com/press',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: 'https://yourapp.com/privacy',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.3,
},
{
url: 'https://yourapp.com/terms',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 0.3,
},
]
}
// app/layout.tsx
<Script
defer
data-domain="yourapp.com"
src="https://plausible.io/js/script.js"
/>
// lib/analytics.ts
export function trackEvent(name: string, props?: Record<string, string>) {
if (typeof window !== 'undefined' && window.plausible) {
window.plausible(name, { props })
}
}
// Usage
trackEvent('Download Click', { source: 'hero' })
trackEvent('Waitlist Signup')
□ App Store button clicks (by section)
□ Email signups
□ FAQ expansions
□ Screenshot carousel interactions
□ Scroll depth
□ Time on page
import Image from 'next/image'
// Always use Next.js Image component
<Image
src="/screenshot.png"
alt="App screenshot"
width={390}
height={844}
priority // For above-the-fold images
placeholder="blur"
blurDataURL="data:image/..."
/>
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}
import dynamic from 'next/dynamic'
// Lazy load below-the-fold components
const Testimonials = dynamic(() => import('@/components/Testimonials'))
const FAQ = dynamic(() => import('@/components/FAQ'))
□ All links tested
□ Mobile responsive verified
□ App Store link correct
□ Smart banner app ID correct
□ Meta images uploaded (OG, Twitter)
□ Favicon set
□ Analytics installed
□ Privacy policy page live
□ Terms of service page live
□ Email capture tested
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deploy
vercel --prod
RESEND_API_KEY=re_xxxxx
RESEND_AUDIENCE_ID=aud_xxxxx
NEXT_PUBLIC_APP_ID=123456789
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes