Cloudflare Pages and Workers

Add a contact form to a static site on Cloudflare Pages or Workers static assets, with optional Turnstile and no function to write.

A static site on Cloudflare Pages can only receive a form post if you write a Pages Function to handle it, and then you still need something to send the email. With SilentForms the form posts from the browser to the submit endpoint instead, so the project stays pure static files: no Function, no Worker script and no email binding. Because SilentForms supports Cloudflare Turnstile, this setup also pairs well with Cloudflare's own CAPTCHA.

Pages or Workers#

Cloudflare now recommends starting new projects on Workers, which can serve a folder of static assets the same way Pages does, and Pages remains available for existing and new projects. Everything on this page applies to both: the form is just HTML, and neither platform has to run any code for it. Where the steps differ (environment variables, preview URLs) both are noted.

For a Workers project, a static site needs nothing more than an assets directory in wrangler.jsonc:

{
  "name": "my-site",
  "compatibility_date": "2026-10-01",
  "assets": { "directory": "./dist" }
}

1. Get an access key#

Enter your email on the access key page and click the confirmation link. The key only routes submissions to your inbox, so it is safe in your HTML.

2. Add the form#

Create contact.html in your output folder (or the equivalent page in your generator):

<form action="https://silentforms.com/api/submit" method="POST">
  <input type="hidden" name="accessKey" value="YOUR_ACCESS_KEY_HERE">
  <input type="hidden" name="redirect" value="https://example.com/thanks">

  <label for="name">Name</label>
  <input id="name" type="text" name="name" autocomplete="name" required>

  <label for="email">Email</label>
  <input id="email" type="email" name="email" autocomplete="email" required>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" required></textarea>

  <!-- Honeypot: hidden from people, filled in by bots -->
  <input type="checkbox" name="botcheck" style="display: none;" tabindex="-1" autocomplete="off">

  <button type="submit">Send</button>
</form>

Then add thanks.html alongside it. Both Pages and Workers static assets serve thanks.html at /thanks by default (Pages also redirects /thanks.html to /thanks), so the redirect can use the extensionless URL. It must be an absolute URL on your production domain. Without it, visitors land on the default SilentForms success page. See Redirect after submit.

Framework builds and environment variables#

If Cloudflare builds your site from Git, the access key can come from an environment variable instead of the source. The variable is read during the build and inlined into the HTML or JavaScript; nothing reads it at runtime, so it must be set as a build variable:

  • Pages: in the project, open Settings, then the environment variables section, and add it for Production (and Preview if you test there).
  • Workers Builds: in the Worker, open Settings, Build, then Build variables and secrets. Variables set elsewhere for the Worker's runtime are not visible to the build.

Use your framework's public prefix: NEXT_PUBLIC_ for Next.js, VITE_ for Vite-based apps, PUBLIC_ for Astro and SvelteKit, GATSBY_ for Gatsby. Retry or trigger a new deployment after changing a value, since existing deployments keep the value they were built with. The form code is in the framework guides: Astro, Next.js, SvelteKit.

Add Turnstile#

Turnstile is a natural fit when you are already on Cloudflare. In short:

  1. In the Cloudflare dashboard, create a Turnstile widget and add your site's hostnames to it. You get a site key and a secret key.
  2. Add the widget inside the form. It adds a cf-turnstile-response field when the visitor passes the check:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<!-- inside the form, above the submit button -->
<div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div>
  1. Deploy, then in the SilentForms dashboard turn CAPTCHA on for the form, choose Cloudflare Turnstile and paste the secret key. Do this after the widget is live, otherwise every submission is refused with "Captcha is required."

The full walkthrough, including the JavaScript version, is in Cloudflare Turnstile.

Test it#

Deploy and open the contact page on your pages.dev (or workers.dev) URL. Send yourself a message, check you land on /thanks and check your inbox. In the network tab, the submit request should go to the SilentForms endpoint. If you see a request to your own domain fail instead, the action attribute is wrong.

Preview URLs and domain restriction#

Preview deployments get their own hostnames, such as a hash or branch name in front of my-site.pages.dev. If you restrict the form to your domain in the dashboard, a bare domain covers its subdomains, so adding my-site.pages.dev allows production and all its previews. Do not add pages.dev or workers.dev on their own, which would allow anyone's project. Add your custom domain too. See Restrict to your domain.

Gotchas#

  • A Pages Function or Worker route catches the post. If you have a functions/ folder or a Worker script with a catch-all route, make sure the form's action is the SilentForms URL, not a path on your site.
  • "Access key is required." The build variable was missing when the site built. Check the prefix and where the variable was set, then redeploy.
  • "Captcha verification failed." The Turnstile widget's hostnames do not include the domain you tested on, or the secret saved in the dashboard belongs to another widget.

For every error message, see Troubleshooting.

Next steps#