astro-forms
Astro Forms Skill
Server-side form handling for lead generation sites on Cloudflare Workers.
Scope
This skill covers the backend pipeline: validation → spam check → storage → email → redirect → analytics. For form UI components, field layout, and styling see the project's frontend patterns. The one exception is modifiers.md which documents common form layout variants for reference.
Implementation Sequence
When building a form, follow this order:
- Define the schema —
references/schemas.md→ copy + adapt Zod schema - Copy boilerplate —
assets/boilerplate/lib/→ intosrc/lib/forms/ - Configure Worker —
wrangler.tomlwith KV binding,wrangler secret putfor all secrets - Set up Google Sheet — create service account, share sheet, add column headers (row 1)
- Wire frontend — form posts to
/api/submit, includes Turnstile + honeypot + hidden fields - Test locally —
wrangler dev, submit form, check email + Sheets - Deploy —
wrangler deploy - Audit — run through Conversion Verdict below
Required Policy (non-negotiable)
These apply regardless of tech stack choice:
| Policy | Rule |
|---|---|
| Server-side validation | All input validated on server. Client validation is UX only. |
| Cookie consent | Handled by CookieYes banner (not per-form). |
| Spam protection | Minimum 2 layers required (e.g. honeypot + Turnstile). |
| Storage | Every valid submission persisted (Sheets, DB, or equivalent). |
| Confirmation email | Sent to customer. Failure = log + continue (don't lose the lead). |
| Business notification | Sent to business owner. Failure = alert. |
| Thank-you state | Redirect to /thank-you or show confirmation UI. |
| Analytics event | GTM form_submit event fired. |
| Input sanitisation | User input HTML-escaped before use in email templates. |
| Duplicate protection | Dedupe within 60s window on same email + formId. |
Recommended Stack
| Concern | Tool | Notes |
|---|---|---|
| Validation | Zod | Server-side, type-safe |
| CAPTCHA | Cloudflare Turnstile | Invisible mode |
| Email (primary) | Resend | 3,000/month free tier |
| Email (fallback) | Brevo | Auto-switch on Resend failure |
| Rate limiting | Cloudflare KV | Eventually consistent — fine for form spam control |
| Storage | Google Sheets API | Service account auth, direct API. No webhook. |
| Runtime | Cloudflare Workers | export default { fetch } entry point |
Canonical Submission Schema
One naming convention everywhere: camelCase in TypeScript, maps to storage columns.
type FormSubmission = {
leadId: string; // Generated UUID
formId: string; // e.g. "contact-form"
sourcePage: string; // URL path where submitted
submittedAt: string; // ISO 8601 datetime
name: string;
email: string;
phone?: string;
message?: string;
ipHash: string; // SHA-256 of IP + daily salt, never raw IP
userAgent?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
referrer?: string;
};
All boilerplate, schemas, and references use these field names.
Primary Conversion Declaration
One form per page is THE conversion.
primary_conversion:
type: form
id: "contact-form"
page: "/contact"
All other forms on the page are secondary (newsletter, etc.).
| Page Type | Forms Allowed |
|---|---|
| landing | Primary only |
| service | Primary only |
| contact | Primary only |
| calculator | Contact capture as final step only (calculator logic lives in lead-gen-calculator skill) |
| thank-you | Forbidden |
| 404 | Forbidden |
Progressive Disclosure
Personal data only AFTER value is established.
| Step | Can Ask |
|---|---|
| 1 | Service type, location, non-personal choices |
| 2 | Details, preferences |
| 3+ | Name, email, phone |
Asking email on step 1 is acceptable on high-intent pages (e.g. /contact) but should be avoided on exploratory pages. Use judgement, not a blanket rule.
Spam Protection
All four layers recommended. Minimum two required.
| Layer | Implementation | Required |
|---|---|---|
| Honeypot | Hidden empty field, reject if filled | Yes |
| Time check | Reject if submitted < 3s after page load. Checked in handler, NOT in Zod schema. | Yes |
| Turnstile | Cloudflare invisible CAPTCHA | Recommended |
| Rate limit | 5 submissions per hour per IP via KV | Recommended |
Failure Policy
What happens when part of the pipeline fails:
| Component | On Failure | Rationale |
|---|---|---|
| Zod validation | Return 400 + field errors | Normal flow |
| Turnstile | Return 400 | Likely bot |
| Rate limit | Return 429 | Protect system |
| Duplicate detected | Return 200 (silent OK) | Don't confuse user |
| Google Sheets API save | Log error, continue | Don't lose the lead over storage |
| Customer confirmation email | Log warning, continue | Lead is already captured |
| Business notification email | Log error + alert | Business must know about leads |
| Analytics/GTM | Log warning, continue | Client-side, can't guarantee |
Core principle: never lose a valid lead because a secondary system failed. Email and Sheets failures are logged and alerted, but the user sees success and the lead data is preserved in logs at minimum.
Conversion Verdict
| Condition | Verdict |
|---|---|
| Form on forbidden page (thank-you, 404) | FAIL |
| CookieYes not configured | FAIL |
| No server-side validation | FAIL |
| No confirmation email configured | FAIL |
| No business notification configured | FAIL |
| No thank-you redirect/state | FAIL |
| No GTM event | FAIL |
| No input sanitisation in email templates | FAIL |
Astro SSR API handler reads Astro.locals.runtime.env (removed in v6) |
FAIL |
| Astro SSR API handler without top-level try/catch (silent 500, no logs) | FAIL |
Frontend fetch('/api/name') missing trailing slash when trailingSlash: 'always' |
FAIL |
Deploy command missing --keep-vars (dashboard plaintext vars get wiped) |
FAIL |
| Fewer than 2 spam protection layers | WARN |
| No duplicate protection | WARN |
| Personal data on step 1 (non-contact page) | WARN |
| Rate limiting not configured | WARN |
| All checks pass | PASS |
Locale Support
The skill supports both en-GB and hu-HU locales. Locale affects:
- Zod validation error messages
- Phone number regex pattern
- Email template language
- Postcode format and lookup endpoint
Default locale is en-GB (UK service businesses). See references/schemas.md for locale-specific validation.
Environment Variables
RESEND_API_KEY=re_xxxxx
BREVO_API_KEY=xkeysib-xxxxx
GOOGLE_SERVICE_ACCOUNT_EMAIL=forms@project.iam.gserviceaccount.com
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
GOOGLE_SHEET_ID=1aBcDeFgHiJkLmNoPqRsTuVwXyZ
TURNSTILE_SITE_KEY=0x... # Frontend only
TURNSTILE_SECRET_KEY=0x... # Backend only
IP_HASH_SALT=random-daily-salt
SITE_URL=https://example.com
Critical: all backend env vars are accessed via the env parameter (Workers binding), NOT import.meta.env. The submit.ts handler receives env from the Workers fetch(request, env, ctx) signature. Every boilerplate helper accepts env as a parameter.
If the form runs inside an Astro SSR API route (src/pages/api/*.ts): use import { env } from 'cloudflare:workers' at the top of each handler file. Astro.locals.runtime.env was removed in Astro v6 and throws Worker unhandled exception at runtime. See astro-v6-runtime.md for the full writeup (covers try/catch, --keep-vars, trailing slash, Turnstile hostname allowlist).
For Google Sheets: create a service account in Google Cloud Console, download the JSON key, share the target spreadsheet with the service account email address, then set GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_PRIVATE_KEY, and GOOGLE_SHEET_ID as secrets.
References
- schemas.md — Zod schemas (EN/HU), canonical types
- email.md — Email templates and delivery rules
- resend-setup.md — Resend DNS + account setup guide
- cloudflare-setup.md — Turnstile, KV, Workers
- astro-v6-runtime.md — Astro v6 env access, try/catch pattern,
--keep-vars, trailing slash, Turnstile hostnames - modifiers.md — Form layout variants (UI reference)
- schema-cta.md — CTA structure and tracking
Integration with Tracking Skill
This skill handles the server-side pipeline (validation, storage, email). The soborbo-tracking skill handles client-side conversion tracking (dataLayer, Meta CAPI beacon). They work together:
User clicks Submit
→ <TrackedForm> intercepts (tracking skill)
→ trackLeadSubmit() fires:
• pushes lead_submit to dataLayer (GTM → GA4, Google Ads)
• sends beacon to /api/track (Meta CAPI)
• populates hidden fields (event_id, gclid, UTM)
→ form.submit() fires (native POST)
→ /api/submit receives the POST (this skill)
• validates, dedup, Sheets, email
• event_id links the two systems
Key integration point: The event_id hidden field connects the client-side conversion event with the server-side lead record. Both the Meta CAPI event and the Google Sheets row contain the same event_id, enabling deduplication and attribution reconciliation.
What goes where:
<TrackedForm>wraps the HTML form (tracking skill component)/api/trackreceives the Meta CAPI beacon (tracking skill endpoint)/api/submitreceives the form POST (this skill's endpoint)
Boilerplate
Copy assets/boilerplate/lib/ → src/lib/forms/. The key file is submit.ts which exports a Workers-compatible fetch handler and wires together all other modules. Use ctx.waitUntil() for non-blocking Sheets saves.
Definition of Done
- Schema defined with correct locale
-
submit.tshandler wired up (or Astro API route usingimport { env } from 'cloudflare:workers') - Every API handler wrapped in a top-level try/catch that logs
SRV-FUNC-001 - Env vars configured (local + production)
- Deploy command uses
--keep-vars(Workers Builds → Build configuration) - Turnstile site key set at build time and all hostnames listed in Turnstile allowed list
- Frontend fetches use trailing slash if
trailingSlash: 'always' - CookieYes consent banner configured
- Spam protection: minimum 2 layers active
- Duplicate protection active
- Customer confirmation email sends
- Business notification email sends
- Thank-you redirect works
- GTM event fires
- User input escaped in email HTML
- conversion_verdict = PASS
More from soborbo/claudeskills
astro-seo
SEO markup patterns for Astro lead generation sites. Meta tags, Open Graph, Schema.org, sitemap, robots. Use for all SEO implementation.
37astro-architecture
Technical architecture for Astro lead generation websites. Use when setting up new projects, configuring build tools, or establishing project foundations. For images use astro-images skill. For SEO use astro-seo skill.
20astro-performance
Core Web Vitals and performance optimization for Astro sites. LCP preloading, font strategy, image patterns, critical path, third-party scripts, Cloudflare Tag Gateway. Use for performance tuning.
18astro-animations
Animation patterns for Astro sites. Scroll animations, micro-interactions, transitions, loading states. Performance-focused, accessibility-aware.
17astro-ux
UX patterns and section templates for Astro lead generation sites. Hero, features, testimonials, CTAs, FAQ sections. Use for page section design.
13astro-components
UI component library for Astro lead generation sites. Buttons, forms, cards, badges. Requires design-tokens skill.
10