How the /book Adopt-Assessment form works
The /book page has one lead form — the Adopt Assessment. But this site is static (SSG): every page is pre-built HTML served from a CDN, with no server of our own. So where does a submission go? Here's how it works, down to the details.
The problem: a static site has no server
This site is prerendered at build and served as static files from Cloudflare's edge. Fast and
cheap — but it means there's no backend endpoint to receive a form POST. The classic approach
(a form posting to a server script) isn't available. We need somewhere else to catch the submission.
The fix: Web3Forms (a form backend as a service)
The form POSTs straight from the browser to Web3Forms, a service that accepts the
submission and forwards it to your inbox. There's no server of ours in the middle — just a
client-side fetch:
const res = await fetch('https://api.web3forms.com/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
access_key: WEB3FORMS_KEY, // public — only routes to the inbox, reads nothing
name, email, phone, company, about,
botcheck: false, // honeypot (below)
...attribution, // utm_*, referrer, landing_page — cookieless
}),
})
const data = await res.json()
if (data.success) {
// show "sent", reset the form, fire the generate_lead event
}
The access key is public — and that's fine
The access_key is committed to the repo, visible in the bundle. That's not a leak: a
Web3Forms access key only routes a submission to the configured inbox. It can't read data, can't
sign in, can't sign anything — at worst, someone could send submissions to the same inbox (which is
what the honeypot + Web3Forms' rate limits are for; rotate the key if it's ever abused). It's an
important distinction: not every long string is a secret. A secret is something that can read or
change something.
Spam protection: a honeypot
With no server, we can't run our own checks — so the form plants a honeypot: one hidden field
(botcheck) positioned off-screen and hidden from screen readers. Humans never see it, so never fill
it; many bots fill every field they find. Web3Forms rejects any submission whose honeypot field is
filled.
{/* honeypot — humans never see it; a bot that fills it → rejected */}
<input type="text" name="botcheck" tabIndex={-1} aria-hidden="true"
style={{ position: 'absolute', left: '-9999px', opacity: 0 }} />
Cookieless attribution
When a visitor first lands, the form stores first-touch attribution — utm_*, referrer, and
the landing page — to sessionStorage, then attaches it to every submission. This runs with no
cookie and no consent prompt: sessionStorage isn't a cookie, lives only for the tab, and doesn't
track you across sites. So a lead arrives with its source attached — which campaign, which referrer —
without touching consent.
The conversion signal: generate_lead
When Web3Forms returns success, the form fires the GA4 generate_lead event (via gtag)
carrying the first-touch utm_*. If VITE_GA_ID isn't set, track() is a no-op — so the form works
fine with no analytics at all. And under Consent Mode v2 'denied', GA4 still sends that event
cookieless (no client-id), so we respect consent without losing the aggregate signal.
State & accessibility
The form has four states — idle · sending · sent · error. The button swaps its label and is
disabled while sending (preventing a double-submit). Results are announced to assistive tech:
success uses role="status", errors and the phone-validation error use role="alert". Every field
has a <label htmlFor>, and the phone input ties its error in with aria-invalid +
aria-describedby. The number is validated client-side (^\d{6,15}$) before anything is sent.
CSP: why fetch, not a form action
This site locks down a Content-Security-Policy with no 'unsafe-inline'. Two directives decide
where the form may talk:
Common questions
Is the Web3Forms access key a secret?
No — it's public by design and safe to commit. The access key only routes a submission to the configured inbox; it can't read data, access the account, or sign anything. At worst someone could send submissions to the same inbox — which the honeypot plus Web3Forms' rate limits handle, and the key can be rotated if abused.
How is spam handled without a CAPTCHA?
With a honeypot: one hidden field (botcheck) a human never sees or fills, but many bots fill automatically. A submission with it filled is rejected. Web3Forms also runs server-side spam checks and rate limits. For higher volume, a frictionless CAPTCHA like Cloudflare Turnstile can be added.
Does the UTM/referrer tracking need cookie consent?
No. Attribution is stored in sessionStorage (not a cookie, lives only for the tab, never cross-site) and attached to the submission — so no consent is required. The GA4 generate_lead event is also sent cookieless under Consent Mode 'denied', so the conversion signal still respects the user's choice.
Where this fits
This pattern leans on the static hosting from CI/CD: GitHub Actions → Cloudflare Pages
and the security baseline from SSL & security on the edge (whose
CSP allows this connect-src). The form itself lives at /book.
Sources