> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipnative.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Marketing Landing Page

> Ready-to-use marketing page with waitlist and launch modes

Shipnative includes a marketing landing page built with Vite + React + Tailwind CSS. Use it to collect emails before launch (waitlist mode) or showcase your app after it's live (launch mode).

<Tip>
  **For vibecoders:** `yarn setup` configures this for you. Just run `yarn web` to see your marketing page at localhost:5173.
</Tip>

<Note>
  **Which landing page is this?**

  * `apps/web/` - Your app's marketing page (this guide)
  * `landing_page/` - Shipnative's own marketing site (not yours)
</Note>

## Two Modes

The marketing page has two modes you can switch between:

### Waitlist Mode (Pre-Launch)

Perfect for collecting early user emails before your app launches.

**Features:**

* Email capture form with validation
* Success confirmation message
* App screenshot showcase
* Feature bento grid with 6 tiles:
  * AI-first architecture
  * Mock mode
  * Premium design system
  * Monetization
  * Analytics & reliability
  * Optimized dev loop
* AI tools integration section (Cursor, Claude, Copilot, etc.)
* Fully responsive design
* SEO optimized

**When to use:** Building in public, collecting beta testers, pre-launch hype

### Launch Mode (Post-Launch)

Perfect for when your app is live on the App Store and Google Play.

**Features:**

* App Store and Google Play badge links
* Hero section with app name and tagline
* Feature highlights (3 key benefits)
* Clean, minimal design focused on downloads
* SEO optimized

**When to use:** App is live and you want to drive downloads

## ️ Quick Setup

<Note>
  `yarn setup` now also asks for the marketing page mode. It writes `apps/web/.env` for you, so you can skip to Step 3 if you just ran it.

  **Important:** The Resend API key is now stored securely in Supabase Edge Function secrets, not in the frontend `.env` file. See the "Adding a Waitlist API" section below for setup instructions.
</Note>

### Step 1: Copy Environment Variables (if you skipped `yarn setup`)

```bash theme={null}
cp apps/web/.env.example apps/web/.env
```

### Step 2: Configure Your Settings

Edit `apps/web/.env` (or let `yarn setup` fill it):

```bash theme={null}
# Choose mode: 'waitlist' or 'launch'
VITE_MODE=waitlist

# Your app details
VITE_APP_NAME=YourApp
VITE_APP_DESCRIPTION=Your app description goes here
VITE_APP_URL=https://yourapp.com

# SEO & Social
VITE_OG_IMAGE=https://yourapp.com/og-image.jpg

# Waitlist Mode Settings
VITE_APP_SCREENSHOT_URL=/app-screenshot.png
VITE_WAITLIST_API_ENDPOINT=https://your-project-ref.supabase.co/functions/v1/waitlist  # Supabase Edge Function endpoint

# Launch Mode Settings (required only in launch mode)
VITE_IOS_APP_URL=https://apps.apple.com/app/your-app
VITE_ANDROID_APP_URL=https://play.google.com/store/apps/details?id=com.yourapp
```

### Step 3: Switch Modes Quickly

```bash theme={null}
yarn marketing:waitlist   # sets VITE_MODE=waitlist in apps/web/.env
yarn marketing:launch     # sets VITE_MODE=launch in apps/web/.env
# or: yarn marketing:mode <waitlist|launch>
```

### Step 4: Run the Development Server

```bash theme={null}
yarn web        # alias for `yarn marketing:dev`
# or: yarn marketing:dev
```

Visit `http://localhost:5173` to see your marketing page!

## Customization

### Switching Between Modes

Use the helper command (it updates `apps/web/.env` for you):

```bash theme={null}
yarn marketing:mode waitlist   # Pre-launch waitlist
yarn marketing:mode launch     # Post-launch store links
# Aliases: yarn marketing:waitlist / yarn marketing:launch
```

Prefer to edit manually? Change `VITE_MODE` in `apps/web/.env`. No code changes needed - the app automatically renders the correct mode!

### Customizing Waitlist Mode

Edit `/apps/web/src/components/WaitlistMode.tsx`:

**Change feature tiles** (lines 92-164):

```typescript theme={null}
const bentoTiles: BentoTile[] = [
  {
    title: 'Your Feature Title',
    description: 'Feature description here',
    icon: '',
    pill: 'Optional badge',
    accent: 'from-[#E0F2FE] via-white to-[#C7E9FF]',  // Gradient colors
    span: 'lg:col-span-2',  // Optional: make tile wider
    points: [
      'Key benefit 1',
      'Key benefit 2',
    ],
    metric: {  // Optional: add a metric
      label: 'to launch',
      value: '< 1 hr',
    },
  },
  // Add more tiles...
]
```

**Change AI tools** (lines 33-90):

```typescript theme={null}
const aiTools: AiTool[] = [
  {
    name: 'Tool Name',
    url: 'https://tool.com',
    domain: 'tool.com',
    logoText: 'TN',  // 1-2 letter abbreviation
    logoGradient: 'from-[#0C163D] via-[#0F4C75] to-[#2DD4BF]',
    tagline: 'Short description of integration',
  },
  // Add more tools...
]
```

**Add your app screenshot:**

1. Place your screenshot in `/apps/web/public/app-screenshot.png`
2. Or update `VITE_APP_SCREENSHOT_URL` to use a URL

### Customizing Launch Mode

Edit `/apps/web/src/components/LaunchMode.tsx`:

**Change feature highlights** (lines 92-108):

```typescript theme={null}
<div className="space-y-2">
  <div className="text-3xl">️</div>
  <h3 className="font-semibold text-gray-900">Your Feature</h3>
  <p className="text-sm text-gray-500">Feature description</p>
</div>
```

**Customize colors and styling:**

The landing page uses Tailwind CSS. Edit component files to change:

* Colors: `bg-blue-600`, `text-gray-900`, etc.
* Spacing: `p-4`, `m-8`, `gap-6`, etc.
* Layout: Grid, flex, responsive breakpoints

### Adding a Waitlist API with Supabase Edge Function

The waitlist form is already configured to use a Supabase Edge Function endpoint. This keeps your Resend API key secure on the server side.

**Step 1: Create the Waitlist Table**

The waitlist table is included in the default Supabase schema. You have three options:

**Option A: Use Migrations (Recommended)**
The initial migration already includes the waitlist table:

```bash theme={null}
# Link your project (one-time)
supabase link --project-ref your-project-ref

# Apply all migrations
supabase db push
```

**Option B: Run the full schema**
If you prefer not to use migrations, run the entire `supabase/schema.sql` file in your Supabase SQL Editor. It includes the waitlist table along with all other default tables.

**Option C: Run just the waitlist table SQL**
If you've already set up your database and just need the waitlist table, run this SQL in your Supabase SQL Editor:

```sql theme={null}
-- Waitlist table for storing email addresses
CREATE TABLE IF NOT EXISTS public.waitlist (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email TEXT NOT NULL UNIQUE,
    source TEXT DEFAULT 'marketing_page',
    user_agent TEXT,
    ip_address TEXT,
    email_sent BOOLEAN DEFAULT false,
    email_sent_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable Row Level Security
ALTER TABLE public.waitlist ENABLE ROW LEVEL SECURITY;

-- Allow anonymous inserts (for waitlist form submissions)
CREATE POLICY "Anyone can add to waitlist"
    ON public.waitlist
    FOR INSERT
    WITH CHECK (true);

-- Allow authenticated users to view waitlist
CREATE POLICY "Authenticated users can view waitlist"
    ON public.waitlist
    FOR SELECT
    USING (auth.role() = 'authenticated');

-- Create indexes
CREATE INDEX IF NOT EXISTS waitlist_email_idx ON public.waitlist(email);
CREATE INDEX IF NOT EXISTS waitlist_created_at_idx ON public.waitlist(created_at DESC);
```

**Step 2: Deploy the Edge Function**

1. Install Supabase CLI (if not already installed):

```bash theme={null}
yarn global add supabase
# or: npm install -g supabase
```

2. Login to Supabase:

```bash theme={null}
supabase login
```

3. Link your project:

```bash theme={null}
supabase link --project-ref your-project-ref
```

4. Initialize Supabase in your project (if not already done):

```bash theme={null}
supabase init
```

5. Create the edge function:

```bash theme={null}
supabase functions new waitlist
```

This creates `supabase/functions/waitlist/index.ts`. Replace its contents with this code:

```typescript theme={null}
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
    const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    const supabase = createClient(supabaseUrl, supabaseServiceKey)

    const resendApiKey = Deno.env.get('RESEND_API_KEY')
    const { email } = await req.json()

    if (!email || typeof email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      return new Response(
        JSON.stringify({ error: 'Invalid email address' }),
        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      )
    }

    const userAgent = req.headers.get('user-agent') || ''
    const ipAddress = req.headers.get('x-forwarded-for')?.split(',')[0] || ''

    const { data: waitlistEntry, error: dbError } = await supabase
      .from('waitlist')
      .upsert(
        {
          email: email.toLowerCase().trim(),
          source: 'marketing_page',
          user_agent: userAgent,
          ip_address: ipAddress,
        },
        { onConflict: 'email', ignoreDuplicates: false }
      )
      .select()
      .single()

    if (dbError) {
      return new Response(
        JSON.stringify({ error: 'Failed to save email' }),
        { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      )
    }

    // Send confirmation email via Resend (if configured)
    let emailSent = false
    if (resendApiKey && waitlistEntry) {
      try {
        const resendResponse = await fetch('https://api.resend.com/emails', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${resendApiKey}`,
          },
          body: JSON.stringify({
            from: Deno.env.get('RESEND_FROM_EMAIL') || 'onboarding@resend.dev',
            to: email,
            subject: Deno.env.get('RESEND_WAITLIST_SUBJECT') || 'Thanks for joining our waitlist!',
            html: Deno.env.get('RESEND_WAITLIST_HTML') || `
              <h1>Thanks for joining our waitlist!</h1>
              <p>We'll notify you when we launch.</p>
            `,
          }),
        })

        if (resendResponse.ok) {
          emailSent = true
          await supabase
            .from('waitlist')
            .update({ email_sent: true, email_sent_at: new Date().toISOString() })
            .eq('id', waitlistEntry.id)
        }
      } catch (emailError) {
        console.error('Email sending error:', emailError)
      }
    }

    return new Response(
      JSON.stringify({ success: true, message: 'Email added to waitlist', email_sent: emailSent }),
      { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message || 'Internal server error' }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})
```

6. Deploy the edge function:

```bash theme={null}
supabase functions deploy waitlist
```

7. Set environment secrets in Supabase Dashboard:
   * Go to **Project Settings** → **Edge Functions** → **Secrets**
   * Add `RESEND_API_KEY` (your Resend API key)
   * Add `RESEND_FROM_EMAIL` (optional, e.g., `noreply@yourdomain.com`)
   * Add `RESEND_WAITLIST_SUBJECT` (optional, custom subject line)
   * Add `RESEND_WAITLIST_HTML` (optional, custom HTML email template)

**Step 3: Configure the Frontend**

Set the edge function endpoint in `apps/web/.env`:

```bash theme={null}
VITE_WAITLIST_API_ENDPOINT=https://your-project-ref.supabase.co/functions/v1/waitlist
```

Replace `your-project-ref` with your actual Supabase project reference (found in your Supabase dashboard URL).

**That's it!** The waitlist form will now:

* Save emails to your Supabase database
* Send confirmation emails via Resend (if configured)
* Keep your Resend API key secure on the server

## Deployment

### Deploy to Vercel (Recommended)

1. Push your code to GitHub
2. Go to [vercel.com](https://vercel.com)
3. Import your repository
4. Set **Root Directory** to `apps/web`
5. Add environment variables from your `.env` file
6. Deploy!

The landing page includes a `vercel.json` configuration file for easy deployment.

### Deploy to Netlify

1. Go to [netlify.com](https://netlify.com)
2. Import your repository
3. Set **Base directory** to `apps/web`
4. Set **Build command** to `yarn build`
5. Set **Publish directory** to `apps/web/dist`
6. Add environment variables
7. Deploy!

### Other Platforms

The marketing page is a standard Vite app, so it works with:

* Cloudflare Pages
* GitHub Pages
* AWS Amplify
* Any static host

Build command: `yarn build` (from `apps/web/`)
Output directory: `dist/`

## SEO Configuration

The landing page includes SEO components that set:

* Meta title and description
* Open Graph tags (for social sharing)
* Twitter cards
* Canonical URL

Configure these in your `.env`:

```bash theme={null}
VITE_APP_NAME=YourApp  # Used as page title
VITE_APP_DESCRIPTION=Your description  # Used in meta description
VITE_OG_IMAGE=https://yourapp.com/og-image.jpg  # Social share image
VITE_APP_URL=https://yourapp.com  # Canonical URL
```

**Pro Tip:** Create an Open Graph image (1200x630px) for better social sharing. Place it in `/apps/web/public/og-image.jpg`.

## vs. Main Landing Page Repo

You might be wondering: "What's the difference between `apps/web` and the `landing_page` repo?"

| Feature           | `apps/web`                         | `landing_page`                   |
| ----------------- | ---------------------------------- | -------------------------------- |
| **Purpose**       | Your app's marketing page          | Shipnative's own marketing       |
| **Included**      | Yes, in the monorepo               | Separate repository              |
| **Customization** | You own it - edit freely           | Don't edit (it's for Shipnative) |
| **Use case**      | Pre-launch waitlist or launch page | Shipnative product marketing     |
| **Deploy**        | Your domain                        | shipnative.com                   |

**TL;DR:** Use `apps/web` for YOUR app. Ignore `landing_page` (unless you're contributing to Shipnative itself).

## Tips & Best Practices

### Pre-Launch Strategy

1. Start in **waitlist mode** while building
2. Share the link on Twitter, Product Hunt, Reddit
3. Collect 100-1000+ emails before launch
4. Send launch announcement when app goes live

### Launch Day

1. Switch to **launch mode** (`VITE_MODE=launch`)
2. Add your App Store / Play Store URLs
3. Redeploy
4. Email your waitlist!

### A/B Testing

Want to test different copy? Create multiple deployments:

* `waitlist-v1.yourapp.com`
* `waitlist-v2.yourapp.com`

Deploy the same code with different env vars and see which converts better.

### Custom Domain

1. Buy a domain (Namecheap, Google Domains, etc.)
2. Point it to your Vercel/Netlify deployment
3. Configure in your hosting dashboard
4. Update `VITE_APP_URL` to match

## Troubleshooting

<AccordionGroup>
  <Accordion title="Environment variables not working">
    * Make sure your `.env` file is in `apps/web/`, not the monorepo root
    * Restart the dev server after changing `.env`
    * Vite requires `VITE_` prefix for all public env vars
    * Check for typos in variable names
  </Accordion>

  <Accordion title="App screenshot not showing">
    * Verify the image exists in `/apps/web/public/`
    * Check that `VITE_APP_SCREENSHOT_URL` matches your filename
    * Try using an absolute URL instead of a relative path
    * Clear browser cache
  </Accordion>

  <Accordion title="Waitlist form not submitting">
    * Check browser console for errors
    * Verify `VITE_WAITLIST_API_ENDPOINT` is correctly set
    * Test your API endpoint with Postman/Insomnia first
    * Check CORS settings on your API
  </Accordion>

  <Accordion title="Styling looks broken">
    * Run `yarn install` in `apps/web/`
    * Verify `tailwind.config.cjs` exists
    * Check that `src/index.css` is imported in `src/main.tsx`
    * Try `yarn build` to see if it's a dev-only issue
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Deploy Your Page" icon="rocket" href="https://vercel.com">
    Get your landing page live on Vercel in 5 minutes
  </Card>

  <Card title="Build Your App" icon="mobile" href="/getting-started/quickstart">
    Start building your actual mobile app with vibecoding
  </Card>

  <Card title="Analytics Setup" icon="chart-line" href="/core-features/analytics">
    Track waitlist signups and page views with PostHog
  </Card>

  <Card title="Custom Domain" icon="globe" href="https://vercel.com/docs/custom-domains">
    Point your domain to your landing page
  </Card>
</CardGroup>
