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)框架中被偵測到,以及除錯那些看似提交成功卻從未出現在「表單」介面中的提交紀錄。
常見問題
為什麼我的表單無法被偵測到?
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。被標記為垃圾的提交資料會靜默地移至表單 UI 中的獨立「垃圾郵件」清單中。
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.





首頁
