Build a Lovable landing page with a hero section, feature highlights, social proof, and a waitlist form that saves emails to a Supabase-backed Cloud table. This guide covers spam-resistant form handling, per-page SEO meta (including the TanStack Start SSR default for new projects), and publishing with a custom domain — a launch-ready page in under two hours.
| Fact | Value |
|---|---|
| Tool | Lovable |
| Build time | 1.5–2 hours |
| Difficulty | Beginner |
| Last updated | September 2026 |
What you're building
A landing page's job is narrow but important: convince a visitor your product is worth their email address before it even exists yet. That means every section has to earn its place — a clear hero that states the value proposition in one sentence, features that back up the claim, social proof that reduces perceived risk, and a low-friction form that captures the signup without asking for more than necessary.
The backend side is intentionally minimal: a single Supabase table behind Lovable Cloud, protected by RLS so signups are write-only from the public side. The Edge Function pattern used for the form submission adds honeypot spam protection and clean duplicate-email handling without needing a third-party form service.
SEO gets real attention here because a launch page is often the first thing indexed and shared — this guide covers the practical difference between TanStack Start's default SSR (for projects created after May 13, 2026) and the prerendering approach older Vite SPA projects rely on, so your meta tags and social previews actually work regardless of which stack your project is on.
Final result
A publish-ready landing page with hero, features, social proof, and a spam-resistant waitlist form saving to a Cloud-backed table, complete with per-page SEO meta and a custom domain.
Tech stack
Prerequisites
- A Lovable account (Free plan works for building and testing; custom domain requires a paid plan)
- Lovable Cloud connected to the project (Cloud tab → Database)
- Basic product copy ready: value proposition, 3-4 key features, and any testimonials or affiliations you plan to use as social proof
- A domain name if you plan to publish on a custom domain rather than the free lovable.app subdomain
Build steps
Create the waitlist table in Lovable Cloud
Prompt Lovable to set up the backend table that will store every signup. Keep the schema minimal at launch — email and a timestamp are the only truly required fields — and add optional columns for basic attribution if you're running ads or multiple traffic sources. Set the RLS policy so anyone (including anonymous visitors) can INSERT a new row, but nobody can SELECT the table's contents from the client side — signups should only be readable from Cloud → Database directly or through an authenticated admin view you build separately, never exposed to the public.
1Create a waitlist_signups table with:2- id (uuid, primary key, default gen_random_uuid())3- email (text, not null, unique)4- referrer (text, nullable) — where the signup came from (utm_source or page URL)5- created_at (timestamptz, default now())67RLS policy: allow anonymous and authenticated INSERT with no restrictions on the row values except the unique email constraint. Do NOT allow public SELECT — only accessible via Cloud → Database or a future authenticated admin view.Pro tip: The UNIQUE constraint on email does double duty — it prevents duplicate signups AND gives you a clean way to show a friendly 'you're already on the list!' message instead of a generic database error when someone submits twice.
Expected result: The waitlist_signups table appears in Cloud → Database with the correct columns and RLS policy. TypeScript types are generated automatically.
Build the hero and features sections
Build the top of the page first, since it carries the most weight for conversion. Describe your product's core value proposition in one sentence for the hero headline, a supporting sub-headline, and the primary call-to-action that scrolls to or opens the waitlist form. Follow with a features section highlighting 3-4 key benefits, each with a short headline, one or two sentences of description, and an icon. Keep copy specific to what you're actually building rather than generic marketing language — a landing page's job is to make a stranger understand what the product does and why they should care within a few seconds.
1Build the landing page hero section at the top of src/pages/Index.tsx:23- Full-width section, centered content, max-width container (max-w-4xl)4- Headline (h1, large, bold): the core value proposition in one sentence5- Sub-headline (p, muted-foreground, larger text): one supporting sentence explaining who it's for6- Primary CTA Button: 'Join the Waitlist' — scrolls smoothly to the waitlist form section using an anchor link7- Optional: a hero image, illustration, or simple product screenshot placeholder below the CTA89Below the hero, build a features section:10- Section heading: 'Why [Product Name]'11- 3-4 feature Cards in a responsive grid (grid-cols-1 md:grid-cols-3)12- Each Card: icon (lucide-react), short bold title, 1-2 sentence description13- Keep the grid balanced — an odd number of features (3) tends to look better than an even number that leaves gaps on smaller screensPro tip: Ask Lovable to use Visual Edits (no credit cost) for small copy or spacing tweaks after the initial generation, rather than re-prompting the whole section — it's faster and doesn't consume build credits for cosmetic changes.
Expected result: The page renders a hero section with a working scroll-to-form CTA, followed by a responsive features grid. Layout holds up correctly on mobile widths.
Add a social proof section
Social proof reduces the perceived risk of joining a waitlist for a product that doesn't exist yet. Depending on what you actually have available, build one of: a short testimonial from an early user or advisor, logos of companies or communities you're affiliated with, or a simple 'X people already joined' counter fed live from the waitlist_signups table. If you don't have real testimonials or logos yet, favor a specific, credible claim over generic hype — 'Built by the team behind [previous product]' or 'Currently in private beta with 12 early testers' reads more credibly than vague enthusiasm with no substance behind it.
1Add a social proof section between the features and the waitlist form:23- If using a live counter: query count of rows in waitlist_signups (supabase.from('waitlist_signups').select('*', { count: 'exact', head: true })) and display as '{count}+ people already joined' — only render this section once the count exceeds 20, otherwise hide it entirely4- If using testimonials: a single centered Card with a quote, name, and role/affiliation5- If using logos: a muted, grayscale row of 3-5 logos with the label 'As seen in' or 'Trusted by'Pro tip: A low signup count displayed publicly can undercut credibility rather than build it. If your count is still small, lead with a specific testimonial or affiliation instead and hold off on the live counter until it crosses a threshold you're comfortable showing.
Expected result: The social proof section renders below the features grid. If using the live counter, it correctly reflects the current row count from waitlist_signups and hides itself below the configured threshold.
Build the waitlist form with spam protection
Build the form itself using react-hook-form and zod for validation — a single email field is usually enough friction-wise, though you can add an optional 'what are you hoping to use this for' text field if you want qualitative signal from early signups. On submit, call an Edge Function rather than inserting directly from the client, so you can add a honeypot spam check and basic rate limiting server-side. Handle the duplicate-email case gracefully using the UNIQUE constraint's error code rather than showing a generic failure.
1Build a WaitlistForm component with:2- Email input (required, valid email format via zod)3- Optional hidden honeypot field (name it something plausible like 'company_website', styled with display:none and tabIndex=-1 so real users never see or tab into it)4- Submit Button with a loading state56On submit, call the join-waitlist Edge Function with { email, honeypot, referrer: document.referrer }.78Handle responses:9- Success: show 'You're on the list! We'll email you when we launch.'10- Duplicate email (error code from the function): show 'You're already on the waitlist — we'll be in touch soon.'11- Honeypot triggered: show a generic success message anyway (never reveal to a bot that it was caught) but do not insert the row12- Other errors: show a generic 'Something went wrong, please try again' messagePro tip: Debounce the submit button or disable it immediately on click — without this, a fast double-click submits the form twice before the first request resolves, which the UNIQUE constraint will handle gracefully but still creates a confusing flash of error state.
Expected result: The form validates email format client-side, submits to the Edge Function, and shows the correct message for success, duplicate, and error cases. A honeypot-filled submission shows a fake success message without creating a database row.
Set per-page SEO meta and verify the SSR path
Check which rendering path your project is on before configuring meta tags — this changes what 'correct' looks like. Projects created after May 13, 2026 default to TanStack Start with full server-side rendering, so per-route meta tags (title, description, Open Graph image) are rendered into the initial HTML response and are immediately visible to crawlers and link-preview bots with no special handling. Older Vite SPA projects rely on prerendering for crawlers rather than true SSR — meta tags still need to be set, but verify they're actually appearing in prerendered output rather than only in the client-rendered DOM. Set a specific, keyword-relevant title and description for the landing page route, plus Open Graph tags so the page previews correctly when shared on social platforms or in Slack/Discord.
1Set page-level SEO metadata for the landing page route:2- Title: '[Product Name] — [one-line value proposition]' (under 60 characters)3- Meta description: a specific 1-2 sentence summary of the product and who it's for (under 155 characters)4- Open Graph tags: og:title, og:description, og:image (a 1200x630px preview image), og:type='website'5- Twitter card tags: twitter:card='summary_large_image'67If this is a TanStack Start project, set these via the route's head/meta configuration so they render server-side. If this is a legacy Vite SPA project, confirm with me whether prerendering is correctly picking up these tags for crawlers.Pro tip: Use a real, specific Open Graph image sized 1200x630px rather than your logo alone — link previews with a purpose-built image get noticeably higher click-through when shared than ones with a generic square logo stretched to fit.
Expected result: Viewing the page source (not just the rendered DOM) shows the correct title, meta description, and Open Graph tags. Sharing the URL in Slack or on X shows a proper preview card with title, description, and image.
Publish and connect a custom domain
Once the page, form, and SEO meta are in place, click the Publish button in the top-right corner of the editor. Your app is live on a free xxx.lovable.app subdomain immediately after publishing. To use your own domain, go to Project Settings → Domains (paid plans only) and add your domain. Lovable will show DNS records to add at your registrar — typically an A record pointing to Lovable's IP and a TXT record for verification. Automatic setup via Entri is available for supported registrars; otherwise add the records manually. DNS propagation can take up to 72 hours, though it's usually much faster. Lovable provisions a free SSL certificate automatically once the domain verifies.
Pro tip: Test the waitlist form on your published URL, not just the preview panel, before sharing the link publicly — this catches any environment-specific issues (like an Edge Function secret that wasn't set) before real visitors hit them.
Expected result: The landing page is live on your custom domain with a valid SSL certificate (padlock icon in the browser). Submitting the waitlist form on the live domain successfully creates a row in waitlist_signups.
Complete code
1// supabase/functions/join-waitlist/index.ts2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'45const cors = {6 'Access-Control-Allow-Origin': '*',7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',8 'Content-Type': 'application/json',9}1011const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/1213serve(async (req: Request) => {14 if (req.method === 'OPTIONS') return new Response('ok', { headers: cors })1516 try {17 const { email, honeypot, referrer } = await req.json()1819 // Honeypot check: real users never fill this hidden field. Silently20 // pretend success so bots don't learn the field is being checked.21 if (honeypot) {22 return new Response(JSON.stringify({ ok: true }), { headers: cors })23 }2425 if (!email || typeof email !== 'string' || !EMAIL_REGEX.test(email)) {26 return new Response(27 JSON.stringify({ error: 'invalid_email', message: 'Enter a valid email address.' }),28 { status: 400, headers: cors }29 )30 }3132 const supabase = createClient(33 Deno.env.get('SUPABASE_URL') ?? '',34 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''35 )3637 const { error } = await supabase.from('waitlist_signups').insert({38 email: email.trim().toLowerCase(),39 referrer: typeof referrer === 'string' ? referrer.slice(0, 500) : null,40 })4142 if (error) {43 // Postgres unique_violation44 if (error.code === '23505') {45 return new Response(46 JSON.stringify({ error: 'duplicate', message: "You're already on the waitlist." }),47 { status: 409, headers: cors }48 )49 }50 console.error('join-waitlist insert error:', error)51 return new Response(52 JSON.stringify({ error: 'server_error', message: 'Something went wrong. Please try again.' }),53 { status: 500, headers: cors }54 )55 }5657 return new Response(JSON.stringify({ ok: true }), { headers: cors })58 } catch (err) {59 console.error('join-waitlist error:', err)60 return new Response(61 JSON.stringify({ error: 'server_error', message: 'Something went wrong. Please try again.' }),62 { status: 500, headers: cors }63 )64 }65})Customization ideas
Confirmation email on signup
Connect the native Resend connector or Lovable Emails and prompt Lovable to send a short confirmation email whenever a new row is inserted into waitlist_signups — 'Thanks for joining! We'll email you the moment we launch.' Trigger it from the same join-waitlist Edge Function right after a successful insert.
Password-protected admin view
Add a simple authenticated /admin/waitlist page that lists all signups sorted by date, with a CSV export button. Restrict access to a specific admin email or role so signup data stays private even from other authenticated users.
Referral tracking
Add a referral_code column to waitlist_signups and generate a unique shareable link per signup (?ref={id}). Show each signup's position on the waitlist and how many people they've referred, a proven mechanic for pre-launch viral growth.
A/B testing the hero headline
Store 2-3 hero headline variants in a site_settings table and randomly assign visitors to one on page load, tracking which variant they came from in the referrer field on signup. Compare conversion rates once you have enough signups to be statistically meaningful.
Launch countdown with automatic reveal
Add a countdown timer to a specific launch date, and once the countdown hits zero, automatically swap the waitlist form for a 'We're live!' message linking to the real product — useful if you want the same page to work before and immediately after launch without manual intervention.
Common pitfalls
Pitfall: Inserting waitlist signups directly from the client instead of through an Edge Function
How to avoid: Route the form submission through an Edge Function that checks the honeypot field and validates the email format before inserting, exactly as shown in the complete code above.
Pitfall: Making the RLS policy on waitlist_signups allow public SELECT
How to avoid: Restrict SELECT to an authenticated admin role only, or don't expose a SELECT policy at all and view signups directly in Cloud → Database instead.
Pitfall: Setting SEO meta tags without checking whether the project uses TanStack Start SSR or a legacy Vite SPA
How to avoid: Check the Code tab to confirm your project's stack, and verify the rendered meta tags by viewing page source (not the browser DevTools Elements panel, which shows the post-hydration DOM) before publishing.
Pitfall: Displaying a live 'X people joined' counter before the count is high enough to feel credible
How to avoid: Hide the live counter behind a threshold (for example, only render it once the count exceeds 20-30) and use qualitative social proof — a testimonial or affiliation — until then.
Best practices
- Keep the waitlist form to one required field (email). Every additional required field measurably reduces signup completion, especially on mobile.
- Never expose the waitlist_signups table to public SELECT via RLS — signups are personal data (email addresses) and should only be readable by you or an authenticated admin view.
- Route form submissions through an Edge Function rather than inserting directly from the client, so you have a place to add honeypot and rate-limit checks server-side.
- Set per-page SEO meta (title, description, Open Graph image) before publishing, not after — link previews get cached by some platforms the first time a URL is shared.
- Check whether your project is on TanStack Start SSR or a legacy Vite SPA before assuming your meta tags are crawler-visible — the correct implementation differs between the two.
- Hold off on displaying a live signup counter publicly until it's past the point where a small number would undercut credibility — lead with qualitative social proof until then.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a waitlist landing page with a Supabase backend. The waitlist_signups table has email (unique), referrer, and created_at columns. Write the TypeScript code for a Deno Edge Function that validates an email with zod, checks a honeypot field, catches the Postgres unique-violation error code (23505) for duplicate emails, and returns distinct JSON responses for success, duplicate, and honeypot-triggered cases.
Add a simple password-protected admin page at /admin/waitlist that lists all rows from waitlist_signups in a table sorted by created_at descending, with a button to export the full list as a CSV file. Protect the route so it's only accessible to an authenticated admin user, not the general public.
In Supabase, write the RLS policy SQL for a waitlist_signups table that allows anonymous INSERT from anyone, but blocks all SELECT, UPDATE, and DELETE from anonymous and authenticated non-admin users. Only a specific admin role (checked via a custom claim or a separate admins table) should be able to SELECT the full list.
Frequently asked questions
Does Lovable's landing page output work for SEO?
Yes, and it's much stronger than it used to be. Projects created after May 13, 2026 default to TanStack Start with full server-side rendering, meaning crawlers receive fully rendered HTML — including per-page meta tags — on first request, no different from a traditional server-rendered site. Older projects built as a Vite SPA rely on crawler prerendering rather than true SSR; they still work for SEO but are worth upgrading if organic search matters to your launch. Check your project's stack in the Code tab if you're unsure which one you're on.
How do I stop bots from spamming my waitlist form?
The Edge Function pattern in this guide includes a honeypot field — a hidden input real users never fill in, but simple bots often do — which silently rejects the submission without a visible error. For a public launch expecting meaningful traffic, also add a basic rate limit (reject more than a few submissions per IP per minute) at the Edge Function level, and consider a CAPTCHA only if honeypot filtering proves insufficient in practice; most low-to-moderate traffic launches don't need one.
Can I add a countdown timer or 'launching in X days' element?
Yes — describe the exact launch date and Lovable will generate a countdown component using standard JavaScript Date calculations, no external library required for a simple day/hour/minute countdown. Store the launch date as a constant or, if you want to change it without redeploying, as a value in a site_settings table read on page load.
How many people typically need to see a waitlist counter before it feels credible?
There's no universal number, but showing a raw count under roughly 20-30 signups often reads as sparse rather than reassuring. A common approach: don't display the live counter publicly until you've crossed a threshold you're comfortable with, and until then show qualitative social proof instead (a short list of who's already interested, or a value-proposition-focused testimonial) rather than a small number that undercuts the page's credibility.
Should the waitlist form send a confirmation email?
It's not required for the form to function, but it meaningfully improves the experience — a confirmation email reassures the signup their submission went through and gives you a second touchpoint before launch. Lovable's native Resend connector or built-in Lovable Emails feature both handle this well; see the customization ideas below for the exact prompt pattern.
How do I see who signed up for my waitlist?
Open Cloud → Database in the Cloud tab and select the waitlist_signups table — every row is visible there with email, signup timestamp, and any UTM/referrer data you captured. For anything beyond occasional manual checking, prompt Lovable to add a simple password-protected admin page that lists and exports signups as CSV, covered in the customization ideas section.
Can RapidDev help me turn a waitlist landing page into a full product?
Yes — this is one of our most common engagements. Once a waitlist validates demand, the next step is usually the actual product behind it: auth, billing, and the core feature set. RapidDev picks up directly from a Lovable-built landing page and Cloud schema without needing to rebuild the front end, scoping the buildout based on what the waitlist data and early signups tell you about demand.
Talk to an Expert
Our team has built 1,000+ apps. Get personalized help with your project.
Book a free consultation