Blog
File uploads on a static site: your real options
· 3 min read
Text fields are easy - the hard question arrives with the brief that says "and applicants should attach a CV." A static site can't receive a file; there's no server to receive it. Here are the actual options, from worst to best.
Option 1: "email it to us" (the surrender)
Ask users to send attachments by email. It works, technically, the way a fax machine works: the file arrives disconnected from the form data, into an inbox, with no structure and no size control. For anything recurring - job applications, support attachments, print files - it turns into manual filing forever.
Option 2: direct-to-cloud-storage uploads (the DIY route)
Front-end JavaScript can upload straight to object storage (S3-style presigned URLs). It's a legitimate architecture - and a surprising amount of work to do safely: something has to generate those presigned URLs (so you're writing a serverless function after all), you're implementing size and type validation yourself, connecting the uploaded file to the form submission it belongs to, and deciding where in the world that bucket lives, which matters the moment the uploads contain personal data like CVs.
Choose this when uploads are your product. For "attach a file to a form," it's a lot of plumbing.
Option 3: multipart through a form backend (the boring, correct one)
HTML has supported file fields since roughly forever:
<form action="https://formhook.app/f/fh_your-key"
method="POST" enctype="multipart/form-data">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<input type="file" name="attachment">
<button type="submit">Send</button>
</form>The one thing people forget is enctype="multipart/form-data" - without it the browser sends the filename instead of the file. With a form backend receiving the POST, the file is stored with its submission: open the enquiry in the dashboard, the attachment is right there. Size and type limits are enforced at the endpoint, and spam protection applies to the whole submission, files included.
In Formhook, file uploads are available on the Pro plan and up, and - the part that matters for European sites - uploaded files are stored in Cloudflare R2's EU region, so a CV attached by an EU applicant gets the same data-residency answer as the text fields: it never leaves the EU. (Storage details in the docs.)
Practical rules whichever option you pick
- Constrain the input. Use the
acceptattribute (accept=".pdf,.png,.jpg") as UX, but treat it as advisory - real validation happens at the endpoint. - Tell users the limit before they hit it. "Max 10 MB" next to the field beats an error after a slow upload.
- Files are personal data too. A CV is arguably more sensitive than the form fields around it. Retention, deletion, and export policies must cover attachments, and your privacy policy's storage-location claim includes them. (See the GDPR checklist.)
- Keep the no-JS path working. A plain multipart form posts fine without any script; add AJAX polish on top, not instead.
The pattern for static sites is settled: the site stays static, the form backend receives the multipart POST, and the file lands next to the message it came with. The form is still one attribute - plus one enctype.
Ship a working form in one line
EU-hosted, submissions kept forever, push notifications on every tier.
Start freeNo credit card · read the docs
Keep reading
- What is a form backend? (And when you need one)A form backend receives, stores, and forwards your website's form submissions so you don't run a server. Here's how they work and when you need one.
- GDPR-compliant contact forms: a practical checklistWhat GDPR actually requires from a contact form: lawful basis, data residency, retention, and privacy policy wording. A practical checklist for developers.
- How to add a contact form to GitHub PagesGitHub Pages can't run server code, but you can still have a working contact form. Three approaches compared, with a copy-paste solution that takes a minute.