Eleventy form submissions without a backend server
Handle Eleventy form submissions with a hosted endpoint, reusable Nunjucks partial, and exact domain allow-listing for preview and production.
If you need Eleventy form submissions on a static site, the clean path is a plain HTML form that posts to a hosted endpoint. Eleventy renders the markup; the endpoint accepts the POST, stores the email, and keeps bots out — without turning your 11ty deploy into an app server.
Eleventy (11ty) is a static site generator: build time turns templates into HTML, CSS, and assets. The deployed site has no Node process waiting for form posts. If the page only needs contact or newsletter intake, you do not need to bolt on server code just to catch an email.
Static output still needs an external action
Relative actions that expect PHP, a same-origin API route, or a local mailto:habit break on pure static hosts (GitHub Pages, Cloudflare Pages, Netlify, S3, many CDNs). Eleventy does not invent a form backend. You either add serverless functions on the host, wire a third-party embed, or point plain HTML at a dedicated subscribe endpoint.
Common patterns for Eleventy forms
- Host serverless (Netlify Functions, Pages Functions, etc.). Full control — and you own validation, rate limits, spam, storage, and export.
- Host-tied form products. Fast on one platform; awkward when you move hosts or want the list under your own export path.
- Simple Signups. Campaign-scoped intake on a fixed HTTPS origin. Keep the 11ty site static; the form posts cross-origin to Simple Signups. That is signup/contact intake — not a general replacement for every server action you might run later.
Drop-in HTML form
Build-time HTML is enough. Set method="POST", point action at the Simple Signups subscribe URL, include the public campaign id (pub_...), and keep the honeypot field empty for humans.
<form action="https://simple-signups.com/api/subscribe" method="POST"> <input type="hidden" name="campaignId" value="pub_your_campaign_id" /> <input type="email" name="email" required placeholder="you@example.com" /> <input type="text" name="hp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true" /> <button type="submit">Send</button></form>Field reference and response shapes live in the docs quickstart.
Nunjucks partial + site data
Most 11ty sites use Nunjucks (or Liquid). Put the form in an include, pass the endpoint and campaign id from global data or environment-backed config, and reuse the partial on contact, footer, and landing layouts.
{# _includes/signup-form.njk #}<form action="{{ simpleSignups.endpoint }}" method="POST"> <input type="hidden" name="campaignId" value="{{ simpleSignups.campaignId }}" /> <input type="email" name="email" required placeholder="you@example.com" /> <input type="text" name="hp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true" /> <button type="submit">{{ label | default("Send") }}</button></form>
{# In a layout or page #}{% include "signup-form.njk" %}
{# _data/simpleSignups.js (or .json) — example shape #}module.exports = { endpoint: "https://simple-signups.com/api/subscribe", campaignId: "pub_your_campaign_id",};Swap Liquid {% render %} or a JS template partial the same way — the browser only needs the final HTML fields. Prefer one public campaign per list so allow-list and export stay clean.
When host functions still make sense
Use Netlify Functions, Pages Functions, or another server-side hook when the form needs to do more than intake — for example, write into app-specific data, call a private service, or combine the submit with authenticated user state. For plain contact, waitlist, or newsletter capture, the hosted endpoint is the smaller, more portable default.
Exact domain allow-list: local, preview, production
Each campaign stores an exact list of allowed hostnames. On subscribe, the Worker checks browser Origin (or Referer) against that list. Matching is exact after normalization — no *.example.com wildcards, and apex does not automatically include www.
Plan entries for every host that will embed the form:
- Production — e.g.
www.example.comand bareexample.comif both serve the site - Preview / staging — the real preview hostname your CI or host assigns (often a unique subdomain). Add hosts you actually test from; there is no blanket preview wildcard
- Local — only if you POST from a browser origin such as
localhost:8080during development; omit it if you only smoke-test in preview
Empty allow-lists deny third-party embeds. First-party traffic on the Simple Signups product host is handled separately; cross-site embeds from your Eleventy deploy always need an explicit entry. Details: anti-abuse & domains.
What this does not replace
Simple Signups is the intake layer — domain gate, honeypot and rate limits, optional Turnstile, storage, export. It is not Eleventy middleware, a CMS, or a general multi-step app API. Keep build pipelines and content in 11ty; point marketing and contact forms at a dedicated campaign.
Related
- Contact forms use case — product-shaped guide for site contact and inquiry intake.
- HTML form submissions without PHP — host-agnostic static form pattern.
- Signups on Cloudflare Pages — same intake idea on a Pages deploy.
- Docs quickstart, path-style endpoint, and anti-abuse & domains.