RG Forms logoRG Forms
AI-built sites

Adding a contact form to an AI-generated website

There’s a specific moment that catches people out. The AI built the site, the design is genuinely good, the contact form has nice focus states and validation — and then the first real enquiry vanishes into nothing, because no backend was ever created.

This is the single most common gap in AI-generated sites, and it’s the easiest to close.

Last updated August 19, 2026

The short answer

AI website builders generate the markup and styling for a contact form but not the backend that receives it, so the form usually looks finished and silently does nothing. To make it real, give it a hosted endpoint and have the submit handler POST to it. RG Forms creates that endpoint inside your own Google account in about ninety seconds and exports an RGFORMS.md spec file listing your endpoint URL, form tabs, exact field keys and calling convention — drop that file into your project and an AI coding assistant can wire the form up correctly without guessing.

Why it happens

A generated form is usually one of three things: a bare <form> with no action, an onSubmit handler that logs to the console or fakes a delay with setTimeout, or a TODO comment where the fetch should be. The model produced exactly what it was asked for — the interface — and the part it can’t produce is an endpoint that exists on the internet with your data behind it.

So test before you launch. Submit your own form and check that the message actually reached you. If nothing arrives, the backend is missing, not broken.

Wiring it up

  1. Create the endpoint

    Sign in to RG Forms with Google and define fields that match the form your builder generated — same labels, same order. Takes about ninety seconds and produces a permanent endpoint URL.

  2. Export RGFORMS.md and drop it in the repo

    From your dashboard, download RGFORMS.md — a spec file listing your endpoint URL, every form tab, the exact field keys, the text/plain calling convention, and the honeypot and captcha snippets. Put it at the root of the project.

  3. Ask your AI assistant to wire it up

    In Claude Code, Cursor, Copilot, Windsurf or whatever you’re using, say: *“Read RGFORMS.md and connect the contact form on the homepage to the endpoint, with sending, success and error states.”* The spec file is what stops it from inventing an API shape.

  4. Verify with a real submission

    Submit the form on the deployed site and confirm the row lands in your Google Sheet and the notification hits your inbox. If nothing arrives, check the browser console — a CORS error means something set Content-Type: application/json, which is the one mistake assistants reliably make here.

    Test the endpoint from your terminal
    curl -L -X POST "https://script.google.com/macros/s/AKfycb.../exec" \
      -H "Content-Type: text/plain" \
      -d '{"tab":"contact","fields":{"name":"Ada","email":"ada@example.com","message":"Hello"}}'
    
    # → {"result":"success"}   and a new row appears in your Google Sheet

If you’d rather paste the code yourself

Most AI-generated sites are React — Next.js, Vite, or a builder-specific wrapper. The component below drops in as-is; the React guide covers the variations. If your site is plain HTML, use the HTML version.

ContactForm.tsx
import { useState } from 'react';

const ENDPOINT = import.meta.env.VITE_FORM_ENDPOINT; // or process.env.NEXT_PUBLIC_FORM_ENDPOINT

export default function ContactForm() {
  const [state, setState] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = event.currentTarget;
    setState('sending');

    try {
      const res = await fetch(ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'text/plain' }, // no preflight
        body: JSON.stringify({
          tab: 'contact',
          fields: Object.fromEntries(new FormData(form)),
        }),
      });
      const data = await res.json();
      setState(data.result === 'success' ? 'sent' : 'error');
      if (data.result === 'success') form.reset();
    } catch {
      setState('error');
    }
  }

  if (state === 'sent') return <p>Thanks — we&apos;ll be in touch.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input type="email" name="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <input type="text" name="_hp" tabIndex={-1} autoComplete="off" aria-hidden="true"
             style={{ position: 'absolute', left: '-9999px' }} />
      <button disabled={state === 'sending'}>
        {state === 'sending' ? 'Sending…' : 'Send'}
      </button>
      {state === 'error' && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  );
}

Common questions

Which AI builders does this work with?

All of them — v0, Lovable, Bolt, Replit, Framer exports, or anything Claude, Cursor or Copilot generated. The integration is a browser fetch call, so it doesn’t care what wrote the markup.

What exactly is RGFORMS.md?

A plain-English spec of your project: endpoint URL, tab names, field keys and types, required flags, the calling convention, and copy-paste snippets. It exists so an AI assistant working in your codebase has the real API in front of it instead of guessing at one.

My AI assistant wrote the fetch with `Content-Type: application/json`. Why did it break?

That header triggers a CORS preflight OPTIONS request, and Apps Script web apps can’t answer OPTIONS, so the browser blocks the call before it’s sent. Change it to text/plain — the body stays JSON and the script parses it identically.

Can the AI create the RG Forms project too?

No — provisioning needs a real Google sign-in from you, and that’s deliberate: it’s what keeps the endpoint and the data inside your own account rather than someone else’s.

More answers in the full FAQ.

Your endpoint goes live in ~90 seconds