A Voice-Note Contact Form on a Fully Static Website
How a static Astro site accepts spoken messages, transcribes them while the visitor waits, prefills the form, and emails the note with a private playback link.
- Business type
- Own product — the widget on this site
- Location
- The Woodlands, Texas
- Started
- 2026
- Last updated
- July 19, 2026
- Cloudflare Workers
- Cloudflare R2
- Cloudflare Email Routing
- Workers AI
- Groq
- Whisper large v3
- Astro
You can test this one without taking my word for anything. The button in the corner of this page — Message Alex — is the system described below, running in production. Press it, hold record, and say something. What you get back is the transcript of your own voice, with your email and website already filled in if you said them.
The site itself is a fully static Astro build with no server. Everything here runs in a single Cloudflare Worker on a separate subdomain, with Groq doing the speech and extraction work.
Why a voice note at all
The people this site is written for — owners of established service companies — describe their problem far better out loud than in a form field. Ask them to type and you get “need a new website.” Ask them to talk for ninety seconds and you get the actual situation: what the current site does wrong, which services matter, what happened with the last agency.
A text box collects the short version. The question was whether the longer version could be collected without asking anyone to install, sign up, or schedule a call.
The other half is unglamorous: a voice note that arrives as an audio file is a task, not a message. It cannot be skimmed, searched, or read on a phone in a queue. Whatever captured the speech had to also turn it into text before it reached my inbox.
How it works
- The visitor records in the browser
No install, no permissions beyond the microphone, up to three minutes.
- The audio uploads to a private bucket
Stored under an unguessable name, tagged with the page it came from.
- It is transcribed while the visitor waits
Deliberately synchronous — the transcript has to come back, because it is the thing the visitor is about to edit.
- Contact details are pulled out of the speech
A spoken email or website is normalized and, critically, checked against the transcript before it is trusted.
- The form fills itself in
Transcript in the message box, email and website in their fields. Everything is editable — nothing has been sent yet.
- The visitor edits and submits
This is the moment the email is sent, not the moment they finished speaking.
- The note arrives as readable text
Plus a private link to hear the original in the sender's own voice, and a reply-to address so hitting reply just works.
Recording and sending are two separate steps
This is the design decision the rest follows from.
Transcription happens on upload, before the visitor submits anything. That costs a wait of a few seconds — but it means the transcript is not a byproduct filed away somewhere, it is the draft the visitor then edits. Speech-to-text gets names and company names wrong; letting someone fix their own name before it reaches me is worth more than saving them the wait.
It also means an abandoned recording is exactly that. Nothing is emailed until the form is actually submitted.
The email is text first, audio second
The note arrives as plain text: the message, the fields, the page it came from, the country. Underneath is one line with a link to hear the original recording, valid for thirty days.
The reply-to header is set to the visitor’s address when one was given or spoken, so replying works without copying anything out of the body. Tone, hesitation, and emphasis stay available in the recording for the cases where they matter — but reading comes first.
Architecture
The site is static and stays static. That was a constraint worth protecting: static pages are fast, cheap, and cannot break in the ways a server can.
- Static Astro site on Cloudflare Pages
No server-side rendering, no adapter, no runtime on the site itself.
- One Worker on a separate subdomain
Three endpoints: upload-and-transcribe, submit-note, and playback. All form, voice, and email logic lives here.
- R2 for the recordings
Private bucket. Objects carry the originating page and country as metadata.
- Groq for transcription and extraction, Workers AI as failover
Whisper large v3 and gpt-oss-120b on Groq; a Workers AI binding takes over if Groq is unreachable, so an outage degrades the feature instead of breaking it.
- Cloudflare Email Routing to deliver
Multiple destinations attempted in order, so one misconfigured mailbox is not a lost lead.
The browser talks to a different origin than the page it is on, which makes CORS part of the design rather than an afterthought. Requests from the site’s own origins are allowed; requests carrying no origin at all — a click on a playback link inside an email client, for instance — are also allowed, because that is a legitimate path that no browser will label.
Documented in depth
Reaching everyone
Without JavaScript, the contact form is an ordinary HTML form that posts to the Worker and redirects back to a URL fragment, where a CSS rule reveals the confirmation banner. No script runs at any point. The voice recorder is not available in this mode, but sending a message is.
On iPhone, Safari records in a different audio container than Chrome does. Both are accepted, along with two more formats, because a visitor should never be told their phone is unsupported.
When transcription fails entirely, the recording still uploads, the note still sends, and the audio link still works. The transcript is simply empty. Every AI call in this system is allowed to fail without taking the message with it — the point is to deliver a lead, not to demonstrate a model.
Keeping it from being abused
A hidden field is present in the form that no human ever sees. Bots fill in every input they find, so a submission with that field populated gets a cheerful success response and is silently discarded — no error to learn from, no signal to adapt to.
Uploads are bounded at both ends: too small to be speech is rejected, too large is rejected, and only four audio types are accepted. Field lengths are capped. A submission with no way to reply at all is refused.
Playback links carry no signature and no expiry parameter. The link is the credential: a random name long enough to be unguessable, with age checked against the file’s own upload time on the server. There is nothing in the URL to tamper with.
Automated rate limiting is not built. Volume is currently low enough that the honeypot and size limits are doing the work, and adding a limiter before there is abuse to shape it against would be guesswork.
What is live today
This system runs in production on this very site — the widget on this page is it. The one item not built is automated rate limiting, and it is marked as such.
Component status 9 live · 1 planned
9 live · 1 planned
| Component | Status | Note |
|---|---|---|
| Browser recording, up to three minutes | Live | Four audio formats including iOS Safari |
| Synchronous transcription | Live | — |
| Transcription provider failover | Live | Second provider on a different platform |
| Contact extraction verified against the transcript | Live | — |
| Editable prefill before sending | Live | — |
| Private playback link, thirty-day life | Live | — |
| Email delivery with reply-to and destination failover | Live | — |
| No-JavaScript form fallback | Live | CSS-only confirmation, no script |
| Honeypot and size validation | Live | — |
| Automated rate limiting | Planned — not built | Not built — no abuse pattern to design against yet |
What we got wrong first
A small model invented contact details that sounded completely plausible
- Initial assumption
- Pulling an email address out of a transcript is a trivial extraction task, so the cheapest, fastest model is the obvious choice.
- Hidden complexity
- Given a transcript where someone named their website but never said an email, the small model returned one anyway — info@ or contact@ at that domain. It was not a hallucination in the obvious sense: it was a reasonable guess, correctly formatted, and completely wrong. Worse, it was invisible. A fabricated address looks exactly like a real one in a form field, and the visitor, seeing their own website spelled correctly beside it, has every reason to trust the form and submit.
- Architectural response
- Two changes. A larger model that follows a strict output schema replaced the small one. More importantly, a rule was added that no prompt can override: an extracted value is discarded unless it literally appears in the transcript, after normalizing spoken forms like 'at' and 'dot'. The check is mechanical, it runs on every result, and a dropped value is logged.
Do not instruct a model not to invent things and consider the problem solved. Where an output can be checked against its own source, check it in code. Prompt wording is a preference; a verification step is a guarantee.
The signed playback URL was three times longer than it needed to be
- Initial assumption
- A private file needs a signed URL: a key, an expiry timestamp, and an HMAC proving the other two were not altered.
- Hidden complexity
- That produced links of roughly a hundred and eighty characters, two thirds of which was a signature. These links go into an email that a human reads. They wrapped across lines, looked like phishing, and carried an expiry the recipient could see but not act on — while the actual expiry logic still had to be verified server-side anyway.
- Architectural response
- The file's name became the credential: sixteen random bytes, which is unguessable, and nothing else. Expiry is derived from the object's own upload timestamp, which the storage layer already records. No key, no signature, no query string — a link short enough to read.
Before signing a URL, ask what the signature is actually protecting. If the only secret is 'which file', a sufficiently random filename is that secret, and the server can answer every other question from data it already holds.
A server error surfaced in the browser as a CORS problem
- Initial assumption
- Cross-origin headers are added to responses, and errors are handled where they occur, so the two concerns do not interact.
- Hidden complexity
- When an unexpected exception escaped the handler, the runtime returned its own bare 500 — which never passed through the code that attaches cross-origin headers. The browser saw a response it was not allowed to read, and reported a CORS failure. So every genuine backend error presented in DevTools as a configuration problem, sending debugging in precisely the wrong direction.
- Architectural response
- The entire router is wrapped so that any thrown error becomes a normal JSON error response that still travels through the cross-origin layer. The browser now receives a readable error saying what actually went wrong.
On a cross-origin API, the error path needs the same headers as the success path. An error the browser cannot read is worse than no error at all, because it actively misdirects.
Outcomes
Operational, not measured. No conversion or volume figures are claimed, because none have been tracked over a defined period.
- A visitor can describe their situation in the way that suits them, without typing it.
- Notes arrive readable and searchable, with the original audio one click away when tone matters.
- Replying works directly from the email, with no copying.
- The marketing site stayed fully static, with all dynamic behavior in one small, separately deployed service.
- Every AI step can fail without costing a lead.
Where this pattern fits
Anywhere the visitor knows more than the form is able to ask, and typing it out is the thing stopping them:
| Here | Elsewhere |
|---|---|
| “Describe the problem with your website” | Intake for legal, medical, insurance, or repair work |
| Spoken email and website | Any detail people say faster than they type |
| Transcript emailed to one person | Transcript into a CRM, help desk, or shared inbox |
| Thirty-day audio link | Retention that matches your record-keeping rules |
The reusable parts are the structure, not the subject: transcribe before submit so the person can correct it, verify anything a model extracts against the source, and let every AI step fail without losing the message.
Alex Taranov mapped the workflow, designed the data model and architecture, and oversaw implementation. AI-assisted tooling supported the coding; the product decisions, review, testing, and final responsibility stayed with Taranov Digital.
About AlexNeed a workflow like this in your business?
You do not have to buy a platform. This work can start with one focused module connected to the CRM, order system, or scheduling tool you already use. Tell me the workflow that is currently living in someone's messages.