PROJECTTEMPLATE

Landing Page with Waitlist

The waitlist page is the smallest real project there is. One page, one form, one table of email addresses. It is also the fastest way to find out whether anyone wants the thing before you spend three months building it. If a hundred strangers hand you their email, build it. If nine people sign up and six of them are you testing the form, you just saved a quarter of a year.

The trap is treating "small" as "doesn't need a spec." A vague prompt like "make me a landing page with email signup" gets you a page that stores emails in the browser, or in a public spreadsheet, or nowhere at all: the form animates nicely and the address evaporates. Emails are personal data. The moment you collect one, you own its storage, its privacy, and the awkward question of what happens when the same person signs up twice.

This spec pins down the parts an agent will otherwise improvise: where the emails actually live, what happens on a duplicate, what the visitor sees after submitting, and what keeps a bot from filling your table with garbage overnight. The copy is yours to write. The plumbing is what you're handing over.

Prerequisites

  • A GitHub account and a Vercel account (both free tiers are fine).
  • A domain name if you want one; the default `.vercel.app` URL works for validating the idea.
markdown
# Project: Waitlist landing page for [product name]

Build a single-page site that describes an upcoming product and collects
email addresses from interested visitors. No other pages, no auth, no CMS.

## Stack (use exactly this unless told otherwise)

- Next.js (App Router) deployed on Vercel.
- Emails stored in [Vercel Postgres / Supabase — pick one and say which].
  NOT in a JSON file, NOT in localStorage, NOT emailed to me per signup.
- No component library. Plain CSS or Tailwind, your choice, keep it small.

## Page content (single page, top to bottom)

1. Headline: [what the product does, in the words a customer would use]
2. Subheadline: [one sentence on who it is for and the pain it removes]
3. Three short benefit blocks: [benefit 1] / [benefit 2] / [benefit 3]
4. The signup form: one email field, one button labeled "[Join the waitlist]".
5. Footer: [your name or company], a mailto contact link, and one line:
   "We store your email to notify you at launch. Nothing else. Unsubscribe
   by replying to any email." Do not collect anything beyond the email.

## Signup behavior (the part that matters)

- Table: `signups` with columns `id`, `email` (unique), `created_at`,
  `source` (default "landing"). The unique constraint on `email` is the
  dedupe mechanism; do not dedupe in JavaScript.
- The form posts to a server route. Validate the email server-side.
  Client-side validation is a convenience, not a defense.
- On success: replace the form with "[You're on the list. We'll email you
  at launch.]" Do not redirect to another page.
- On duplicate email: show the SAME success message. A repeat visitor
  should never learn whether an address is already in the table.
- On invalid email or server error: keep the typed value in the field and
  show one plain-language error line under it.

## Spam and abuse (do all three)

- Honeypot: an extra text input hidden from humans via CSS. If it arrives
  filled, return the success message and store nothing.
- Rate limit the signup route: max 5 requests per minute per IP.
- Reject request bodies over 1KB.

## Configuration

- Database credentials live in environment variables on Vercel. Nothing
  secret in the repo. Provide a `.env.example` listing the variable names
  with placeholder values.

## Explicitly out of scope

- Sending any email (confirmation, welcome, anything). Collection only.
- Analytics beyond a signup count. No third-party tracking scripts.
- Admin dashboard. I will read the table directly for now.

## Done means

- Page renders correctly on a phone (375px wide) and a laptop.
- Submitting a valid email adds exactly one row; submitting it again
  adds zero rows and still shows success.
- The honeypot path stores nothing.
- `git clone`, install, `.env` from the example, and it runs locally.

Adaptation notes:

  • If you already use a mailing-list provider (Buttondown, Mailchimp, ConvertKit), swap the database for their API and keep everything else: the honeypot, the rate limit, and the identical-message-on-duplicate rule all still apply.
  • For a local business, "waitlist" becomes "get notified when we open." Same table, same behavior, different verb.
  • Two products or two audiences: keep one table and vary the source column instead of building two sites. You want the comparison in one query.
  • The classic mistake is skipping the duplicate rule and showing "already registered." That message lets anyone test whether a given address signed up, and now your waitlist doubles as a lookup service for other people's emails.
  • Resist adding pages. The moment there is an About page and a Features page, you are building the product's website instead of testing whether the product should exist.