netlify-forms
netlify/context-and-tools
Netlify에서 호스팅되는 사이트의 서버리스 양식 처리 — 배포 시점에 HTML 양식을 감지하고, 제출된 내용을 저장하며, 스팸을 필터링하고, 알림을 전송합니다. Netlify 사이트에 문의 양식, 잠재 고객 확보 양식, 파일 업로드 양식 또는 뉴스레터 가입 양식을 추가할 때 사용하세요. AJAX 양식 제출 연결; 사용자 지정 감사 페이지 설정; 양식에 허니팟 또는 reCAPTCHA 추가; Next.js, Nuxt, SvelteKit, Astro 또는 Gatsby에서 양식 작동시키기; Netlify API를 통해 양식 제출 내용 읽기; 또는 제출 데이터 누락 문제 디버깅
...모든 것을 확장하십시오netlify-forms 소개
이 스킬은 서버 측 코드를 작성하지 않고도 HTML 양식 제출 내용을 수집할 수 있는 Netlify의 서버리스 메커니즘인 Netlify Forms에 대한 가이드입니다. 이 가이드는 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 키를 안전하게 사용하는 방법에 대해 다룹니다. 또한 파일 업로드, 알림 및 제출(submissions) 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가 SSR의 포괄적 차단(catch-all)에 걸리는 '/'가 아닌 스켈레톤 파일 경로(예: /__forms.html)를 대상으로 해야 합니다.
스팸 필터링은 어떻게 작동하나요?
Akismet은 자동으로 실행됩니다. 허니팟 필드(netlify-honeypot)와 reCAPTCHA를 추가할 수 있습니다. 스팸으로 표시된 제출 내용은 Forms 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.
모든 파일
0개 파일netlify-forms 설치
스킬 파일을 다운로드하여 .claude/skills/ 디렉터리에 압축을 풀어주세요.
ZIP 다운로드저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.
git clone https://github.com/netlify/context-and-tools/blob/main/skills/netlify-forms/SKILL.md # Copy SKILL.md to your .claude/skills/ directory
복사





집
