Contact Form & Hero Ad Video (astradial.com)¶
Working contact form with email delivery to admin@astradial.com, plus an in-page ad-video player on the hero image. Built 2026-07-22, live on astradial.com. Repo: astradial/landing-page (local checkout astradial-landing-page-canonical). Same deploy model as the leads form: push master → auto-deploy in ~2 min, static export, no server — except this feature adds the site's first Cloudflare Pages Function.
Contact form (/contact)¶
components/ContactForm.tsx — client component, fields: name, email, phone (10-digit), Hospital/Hotel/Company location (Google Places autocomplete, same pattern as /leads — deliberately duplicated, not shared), calls/day select, auto-filled pincode/district/state (editable; India Post API fallback when Places wasn't used), trial visit date (<input type="date">), message. On success the form is replaced with "Submitted ✅ Our team will reach you ASAP."
Submission pipeline¶
browser → POST /api/contact (Pages Function, functions/api/contact.ts)
→ validate (name, email regex, 10-digit phone)
→ Turnstile siteverify (secret server-side)
→ relay to https://formsubmit.co/ajax/admin@astradial.com
→ email lands in admin@ (Zoho mailbox)
- Pages Functions: the repo-root
functions/dir is auto-detected and compiled at deploy ("Found Functions directory at /functions" in the build log). No wrangler.toml.scripts/build-static.jsonly stripsapp/api, sofunctions/is untouched. - No credentials anywhere: FormSubmit is account-less and key-less. The only secret is
TURNSTILE_SECRET_KEY(Pages env). Client bundle contains only public keys (Turnstile site key, referrer-locked Maps key).
FormSubmit gotchas (learned the hard way)¶
- It returns HTTP 200 with
success:"false"for rejected requests — you MUST parse the JSON and checksuccess === "true", notres.ok. - Requests without
Origin/Refererheaders are rejected ("open this page through a web server"). Server-side callers must send both (the function sendsOrigin: https://astradial.com). - One-time activation: the first accepted submission emails an "Activate Form" link to the destination inbox; nothing is delivered until it's clicked. Done for admin@astradial.com on 2026-07-22.
- Free tier; if it flakes or volume grows, swap the relay call inside
functions/api/contact.tsfor a real ESP — the client contract (POST /api/contact) doesn't change.
Why not Cloudflare Email Service¶
Email Sending requires the Workers Paid plan ($5/mo) — the account is on free. Until purchased, every /email/sending/* API call returns 2036 Unauthorized (this is a plan gate, not a token-scope problem). astradial.com mail is hosted on Zoho (MX mx.zoho.in, SPF include:zoho.in), so admin@ is a real external mailbox; Zoho SMTP via the platform's mailerService.js pattern is the fallback path if a first-party sender is ever wanted.
Turnstile¶
Widget landing-contact-form (account d1c23d71…, Managed mode), hostnames: astradial.com, landing-page-110.pages.dev, localhost. Site key NEXT_PUBLIC_TURNSTILE_SITE_KEY (plain env — must be present at build time; also in .env.local for dev). Secret TURNSTILE_SECRET_KEY (Pages secret, runtime). The widget renders via the implicit script + <div class="cf-turnstile">; the function verifies the token at challenges.cloudflare.com/turnstile/v0/siteverify with the caller's CF-Connecting-IP.
Local testing¶
next dev serves the form but /api/contact 404s (no Functions in dev). Full loop: npm run build:static && npx wrangler pages dev out/ with a git-ignored .dev.vars containing TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA (Cloudflare's always-pass test secret). Curl checks: {} → 400, fake token → 403, valid body + test token → 200 (and a real FormSubmit send — mind the inbox).
Pages env vars (project landing-page)¶
NEXT_PUBLIC_GOOGLE_MAPS_KEY was only in .env.local until 2026-07-22, so production builds shipped with Places autocomplete silently disabled on /leads and /contact (the code degrades to plain typing by design). It now lives in the Pages env alongside the Firebase vars and the two Turnstile keys. Remember: plain env vars bake in at build time — changing them needs a redeploy.
Hero ad video (in-page player)¶
Hero dashboard image (components/HeroImage.tsx) shows a play badge on all viewports; clicking opens a modal with the ad video (youtube-nocookie.com/embed/IHg1AfAkhGs, autoplay, rel=0). The iframe mounts only on click — zero page-weight until then. No view/like/ subscriber counts are shown; the small YouTube logo + title remain. Agreed endgame: swap the iframe for a self-hosted MP4 + native <video> (poster = hero.webp) once the original ad file is available — full brand control, still free (≤25 MB/file on Pages, else R2).
Mobile (<768px) renders a plain full-width <img>+badge (no framer-motion 3D/scroll transforms — unreliable on phone browsers). Two mobile-only CSS fixes in app/responsive.css: .hero-img was hard-locked to 350px wide (now 100%), and the stacked top spacing (mt-12 + 30px padding + 100px content margin) was trimmed so the image sits just under the CTA buttons.
CTA renames (same release)¶
Hero: "Create Account" → "View Demo" → https://demo.astradial.com. Header (desktop + mobile menu): "Start Free" → "Create Account" (editor signup URL unchanged). Footer/FeatureBox/PricingCards already said "Create Account".