Skip to content

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.js only strips app/api, so functions/ 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)

  1. It returns HTTP 200 with success:"false" for rejected requests — you MUST parse the JSON and check success === "true", not res.ok.
  2. Requests without Origin/Referer headers are rejected ("open this page through a web server"). Server-side callers must send both (the function sends Origin: https://astradial.com).
  3. 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.
  4. Free tier; if it flakes or volume grows, swap the relay call inside functions/api/contact.ts for 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".