netlify-forms
netlify/context-and-tools
Netlify 托管网站上的无服务器表单处理——在部署时检测 HTML 表单、存储提交内容、过滤垃圾信息并发送通知。当向 Netlify 网站添加联系表单、潜在客户捕获表单、文件上传表单或新闻通讯订阅表单时可使用此功能; 配置 AJAX 表单提交;设置自定义感谢页面;在表单中添加蜜罐或 reCAPTCHA;让表单在 Next.js、Nuxt、SvelteKit、Astro 或 Gatsby 中正常工作;通过 Netlify API 读取表单提交内容;或调试表单提交丢失的问题。
...展开全部关于netlify-forms
本技能是一份关于 Netlify Forms 的指南,这是 Netlify 提供的一种无服务器机制,无需编写服务器端代码即可收集 HTML 表单提交数据。 它解决了在 Netlify 上实际注册表单并收集数据时常见的障碍:检测是在部署时通过解析预渲染的 HTML 来完成的,因此,除非配置正确,否则在 JavaScript 渲染或服务器端渲染的应用程序中,表单会悄无声息地失败。本技能将带您逐步了解其完整生命周期以及诸多潜在陷阱。
涵盖的功能包括:使用 data-netlify 属性及唯一表单名称进行基本配置、通过无扩展名的操作路径自定义感谢页面,以及针对 JavaScript 框架(React、Vue、Astro、Next.js、 SvelteKit、Remix、Nuxt、TanStack Start)的关键模式:创建一个静态 HTML 骨架文件(例如 public/__forms.html),其中包含每个表单的隐藏副本并配以匹配的字段名称,以确保构建时检测成功。 文中还说明了 AJAX 提交必须采用 x-www-form-urlencoded 或 multipart/form-data 格式(绝不能是 JSON),服务器端渲染(SSR)中需注意将目标路径指向骨架文件路径而非 '/',通过自动 Akismet 配合蜜罐字段和 reCAPTCHA 进行垃圾信息过滤, 以及通过 Netlify 环境变量(SITE_RECAPTCHA_KEY 和 SITE_RECAPTCHA_SECRET)安全使用自有 reCAPTCHA 密钥——这些密钥应保存在服务器端而非硬编码。此外,本文还涉及文件上传、通知以及表单提交 API。
目标用户是在 Netlify 上部署静态或基于框架网站的前端和全栈开发者,他们需要联系表单、反馈表单、潜在客户捕获表单、新闻通讯表单或文件上传表单。 典型用例包括:配置 AJAX 联系表单、添加反垃圾邮件保护、在服务器端渲染(SSR)框架中检测表单,以及调试那些看似提交成功但从未在表单 UI 中显示的提交请求。
常见问题
为什么我的表单无法被检测到?
Netlify 会在部署时扫描预渲染的 HTML。仅由 JavaScript/SSR 渲染的表单对解析器而言是不可见的;您必须添加一个静态骨架 HTML 文件(例如 public/__forms.html),其中包含每个表单的隐藏副本及匹配的字段名称,然后重新部署。
为什么我的 AJAX 提交虽然成功却始终不显示?
Netlify Forms 不接受 JSON 格式。请使用 application/x-www-form-urlencoded(通过 URLSearchParams)或 multipart/form-data 格式提交。 在 SSR 应用中,fetch 请求必须指向骨架文件路径(例如 /__forms.html),而非 '/'——后者会被 SSR 的通配符拦截器截获。
垃圾信息过滤机制如何运作?
Akismet 会自动运行;您可以添加一个诱饵字段(netlify-honeypot)和 reCAPTCHA。被标记的表单提交会自动转移到 Forms 控制面板中的独立“垃圾信息”列表中。
reCAPTCHA 凭据如何安全处理?
默认情况下,Netlify 会为您配置 reCAPTCHA。若要使用您自己的密钥,请将 SITE_RECAPTCHA_KEY 和 SITE_RECAPTCHA_SECRET 设置为 Netlify 环境变量,这样密钥将保存在服务器端,且绝不会在客户端代码中硬编码。
启用表单检测会影响现有部署吗?
不会。检测功能仅影响未来的部署,因此启用后,必须触发一次新的构建,已发布的表单才会开始收集提交内容。
Mark a form for detection with data-netlify="true" (or the bare netlify attribute — equivalent) on the <form> tag. Forms are detected by parsing the final built HTML at deploy time — there is no runtime API call or backend code. Client-side/JS-rendered/SSR forms are NOT in the built HTML and are never detected on their own; they require a static skeleton file (see below).
Prerequisite: form detection must be enabled once in the Netlify UI (Forms > Enable form detection). Takes effect on the next deploy.
Static HTML form
<form name="contact" method="POST" data-netlify="true"> <p><label>Your Name: <input type="text" name="name" /></label></p> <p><label>Your Email: <input type="email" name="email" /></label></p> <p><label>Message: <textarea name="message"></textarea></label></p> <p><button type="submit">Send</button></p></form>
namesets the form name in the UI and must be unique per site.- At deploy, Netlify strips the
data-netlify/netlifyattribute and injects<input type="hidden" name="form-name" value="contact" />. - Add an
<input name="email">so the notification email'sReply-tois set to the submitter.
JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby)
Two required pieces:
1. Static skeleton file public/__forms.html — a hidden copy of each form with data-netlify="true", a hidden form-name input, and every field the component submits, with names matching exactly (Netlify validates field names against the registered form). Without this file, submissions silently fail.
<!-- public/__forms.html --><form name="pizzaOrder" data-netlify="true" hidden> <input type="hidden" name="form-name" value="pizzaOrder" /> <input name="order" type="text" /></form>
2. The rendered form carries a matching hidden form-name input:
<form name="pizzaOrder" method="post" data-netlify="true" onSubmit={handleSubmit}> <input type="hidden" name="form-name" value="pizzaOrder" /> <input name="order" type="text" onChange={handleChange} /> <input type="submit" /></form>
fetch("/") is intercepted by the SSR catch-all function and never reaches form processing. POST to the static skeleton file itself — /__forms.html — not / or an arbitrary path.
export const prerender = false or output: "server" are never scanned at build time, so their forms are never registered. Put the form on a prerendered page, or rely on the static skeleton file.
Next.js Runtime v5 (Next.js 13.5+): extract form definitions to the static skeleton file and submit via AJAX rather than full-page navigation. See https://docs.netlify.com/build/frameworks/framework-setup-guides/nextjs/overview#v5-breaking-changes
AJAX submission
const handleSubmit = event => { event.preventDefault(); const formData = new FormData(event.target); fetch("/__forms.html", { // static sites may POST to "/"; SSR must target the skeleton file method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(formData).toString() }) .then(() => alert("Thank you for your submission")) // or navigate("/thank-you") .catch(error => alert(error));};document.querySelector("form").addEventListener("submit", handleSubmit);
- Body MUST be URL-encoded. JSON is NOT supported.
- If the rendered form has no hidden
form-nameinput, you MUST include aform-namefield in the POST body. - The honeypot field name and
g-recaptcha-response(if used) must be in the body — automatic withFormData().
File uploads
Add type="file"; optionally enctype="multipart/form-data" on the <form>. For AJAX file uploads, do NOT set a Content-Type header — let the browser set it (with the multipart boundary).
document.forms.fileForm.addEventListener("submit", event => { event.preventDefault(); fetch("/", { body: new FormData(event.target), method: "POST" }) // no headers .then(() => { /* success */ });});
Limits: one file per field (use multiple fields for multiple files) · 8 MB max request size · 30 s upload timeout · after form deletion, uploaded files stay at their direct URL for 24 h. PII uploads need extra security (Very Good Security integration).
Custom success page
Add an action path relative to site root, starting with /. Use extensionless paths — Netlify serves thank-you.html at /thank-you; the .html path returns 404.
<form name="contact" action="/thank-you" method="POST" data-netlify="true"></form>
Custom success alert is only possible via AJAX (substitute the redirect with your own logic).
Spam prevention
All submissions are filtered by Akismet. Passed → Verified submissions; flagged → Spam submissions. Honeypot/reCAPTCHA failures are rejected and appear in neither list.
Honeypot: add netlify-honeypot="bot-field" to the <form> and include a CSS-hidden field of that name. Any value entered → submission quietly rejected.
<form name="contact" method="POST" netlify-honeypot="bot-field" data-netlify="true"> <p class="hidden"><label>Don’t fill this out: <input name="bot-field" /></label></p> <!-- real fields --></form>
Netlify reCAPTCHA 2: add data-netlify-recaptcha="true" to the <form> AND an empty <div data-netlify-recaptcha="true"></div> where it renders. Only ONE Netlify-provided challenge per page — for multiple, use custom reCAPTCHA. For JS-rendered forms, also add the div to the static skeleton file.
Custom reCAPTCHA 2: your own reCAPTCHA snippet + data-netlify-recaptcha="true" on the <form>, plus env vars:
SITE_RECAPTCHA_KEY— site key (scopes: Builds + Runtime)SITE_RECAPTCHA_SECRET— secret (scope: Runtime)
Email notifications & subject line
Default sender: [email protected]. Set subject via a hidden subject input or the Netlify UI (Configuration > Notifications) — not both; the HTML value always overrides the UI.
<input type="hidden" name="subject" value="New lead from %{formName} (%{submissionId})" />
Variables: %{formName}, %{siteName}, %{submissionId}. Forms created before May 5, 2023 carry a [Netlify] subject prefix — remove it by adding the data-remove-prefix attribute to the subject input.
Set up notifications (email/webhook/Slack) in the UI: Configuration > Notifications > Form submission notifications > Add notification.
Reading submissions via the API
Use only documented surfaces. Do NOT invent api.netlify.com endpoints or read tokens from local CLI config files. Reference: https://open-api.netlify.com/#tag/submission/operation/listFormSubmissions
- Page through results using the
Linkheader — code that reads only the first response silently drops the rest. listFormSubmissionsreturns data from old/removed fields no longer shown in the UI.- Query spam with
?state=spam.
Submission summary (field order matters)
The UI summary is derived from field type, not name:
- Title: first non-hidden text
<input>that isn't email-like (type="email", or name matchingemail/mail/from/twitter/sender); falls back to a field namedtitleorsubject. - Body: first
<textarea>.
Field order in the HTML affects what appears in the summary.
Debugging missing submissions
- First suspect: Akismet false positive. A missing legitimate submission is usually spam-flagged — check the Spam list (or API
?state=spam) and mark it verified. Do NOT build a custom recovery function or disable spam filtering as a first resort. - Test submissions get flagged as spam: use a real email (not
[email protected]), write full sentences, don't hammer from one IP. - No submissions at all: confirm form detection is enabled and redeploy.
- SSR/JS forms silently failing: verify the static skeleton file exists with exactly-matching field names and that AJAX targets the skeleton file, not
/. - Missing old-field data: the UI shows only fields from the last deployed form version. Mark old fields
hiddeninstead of removing them to keep them visible; old data remains available vialistFormSubmissions.
Constraints
- Deleting a form is permanent: future submissions return
404, past submissions become unavailable. Export CSV first. - Submitted code is sanitized (
<script>→ escaped entities). - For PII, export and delete data regularly.
- Data is stored in Netlify's database, not accessible except via UI/API/CSV.
Netlify house rules (forms)
These are org conventions and field-learned guardrails, not docs facts — theyare merged into the rendered skill by ctx-gen and are never generated.Extracted from the previous hand-written netlify-forms skill; owned by theskills maintainer.
- In SSR apps (Next.js, Nuxt, SvelteKit, etc.),
fetch("/")is interceptedby the SSR catch-all function and never reaches Netlify's form processing.POST the AJAX submission to the static skeleton file itself (e.g./__forms.html), not to an arbitrary path. - Use only documented surfaces: do not curl
https://api.netlify.com/...with an invented endpoint shape, and do not read tokens out of local CLIconfig files (~/Library/Preferences/netlify/config.json). - When reading submissions via the API, page through results (
Linkheader); code that reads only the first response silently drops the rest. - For JS-rendered and SSR forms, always create the static skeleton file
public/__forms.html: a hidden copy of each form withdata-netlify="true", a hiddenform-nameinput, and every field thecomponent submits — names matching exactly (Netlify validates field namesagainst the registered form). Without this file, submissions silently fail. - Astro routes rendered on demand (
export const prerender = false, oroutput: "server"routes) are never scanned at build time, so their formsare never registered. Put the form on a prerendered page or rely on thestatic skeleton file. - A "missing" legitimate submission is usually an Akismet false positive:check the Spam list (or the API with
?state=spam) and mark it verified.Do not build a custom recovery function or disable spam filtering as afirst resort. - For custom success pages, use extensionless
actionpaths (/thank-you,not/thank-you.html) — Netlify servesthank-you.htmlat/thank-youand the.htmlpath returns 404.





首页
