-
Step 1 · Choose the submission channel: form service vs cloud function
Decide where the data goes first, then pick the least work
The heart of a backend-free form is who receives the submission. Three common channels:
- Third-party form services (Formspree, Getform, Basin, Tally): paste an action URL and you're done — storage, email notifications and spam filtering included. The fastest start, and the free tier is enough for personal use
- Cloud function + spreadsheet (cloud functions → a spreadsheet or docs table): the data stays yours and it's fully customisable, at the cost of a few dozen lines of code
- Email forwarding (
mailto:or the service forwards mail): simplest of all, but hard to analyse — fine for a contact form, nothing more
How to choose:
- Just a "contact us" → form service forwarding email
- Sign-ups or lead capture you want to count → the service's own dashboard, or a cloud function writing to a sheet
- Sensitive data (ID numbers, payment details) → don't use a free third party. Cloud function plus your own storage, encrypted
Compliance floor: the form page must state what you collect, why, and how long you keep it — plus a way to request deletion. Any lead-capture form needs a privacy note.
Checklist
- One submission channel chosen
- You know where the data is stored and who can read it
- The form page carries a privacy note
-
Step 2 · Write the form: fewer fields converts better
Cap required fields at three; validate on the client with specific errors
Field count and conversion rate move in opposite directions. Every extra required field loses you people.
Keep required fields to three: usually "name + contact + what you need". Everything else is optional or collected later.
HTML essentials:
<form action="https://your-form-service/f/xxxx" method="POST"> <label for="name">What should we call you</label> <input id="name" name="name" required maxlength="20"> <label for="contact">Contact (email or handle)</label> <input id="contact" name="contact" required> <button type="submit">Submit</button> </form>Experience details:
- Every
<input>gets a<label>(accessibility, plus bigger tap targets on mobile) - Validate the format client-side (email, phone) and put the error below the field, never in an
alert - Disable the button and show "submitting" so double submits are impossible
- On success, either jump to a dedicated thank-you page or show "got it — we'll reply within X hours". State the timeframe
Don't: add a CAPTCHA until you're actually being spammed — it costs more conversion than it saves. When spam does arrive, add a honeypot (hidden field) or rate limiting.
Checklist
- 3 or fewer required fields
- Errors appear below the field with specific wording
- There's a loading state, and success promises a timeframe
- Every
-
Step 3 · Notifications + spreadsheet + spam control
Notify a human on submit, keep the data exportable, block the junk
A form whose submissions you don't see is as good as no form — someone fills it in, you notice three days later, and they're long gone.
Notifications (at least one):
- Email (built into most form services, the most reliable option)
- A chat bot webhook (POST one line from your cloud function, it lands on your phone instantly)
- Both: email archives, bot alerts
Into a spreadsheet (so you can count things):
Have the cloud function write into a table with fieldssubmitted_at / source_page / source_param (src). Always record the source param — without it you can't tell which channel produced the lead.Three cheap anti-spam measures:
- Honeypot: a CSS-hidden input; if a bot fills it, drop the submission
- Rate limiting: at most 3 submissions per IP per minute
- Server-side validation: re-check required formats on the server. Client-side validation is UX, not security
Backup: submissions are an asset too — export once a month into object storage. Free tiers have caps, and you don't want to discover the limit by losing data.
Checklist
- Notifications arrive within a minute of submitting
- Data reaches the table with the source param
- Honeypot and rate limiting are live
-
Step 4 · Launch + conversion tracking + iteration
form_view → form_submit is the funnel; always split it by channel
Launch: HTTPS is mandatory (nobody fills in a form over http), add
canonical, and putnoindexon the thank-you page so it isn't indexed as an empty page.Conversion tracking (the only metric that matters here):
| Event | Trigger | props |
|---|---|---|
|form_view| The form scrolls into view |src|
|form_start| First field focused |src|
|form_error| Validation fails |field|
|form_submit| Submit succeeds |src|How to read it:
- Low
form_start / form_view→ the form isn't visible enough, or the page isn't persuasive form_errorclustered on one field → that field's hint is unclear- Low
form_submit / form_start→ too many fields, or a required field that makes people hesitate
Splitting by
srcis not optional: the same form converts very differently for traffic from chat apps versus search. Without the split you'll misread it.Iterate monthly: look at the funnel once a month and fix only the step that leaks most. Optimising a form site is subtraction — remove fields, remove copy, remove steps. Not addition.
Checklist
- All 4 events are queryable and carry src
- You can name the current form_submit / form_start ratio
- The thank-you page is noindex
- Low