Update to Netlify
Snapshot Oct 7, 2026 · 00:02 UTC · version 1.6.0
Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.
Instructions updated for netlify-forms
Instruction wording changed from “Guide for using Netlify Forms for HTML form handling. Use when adding contact forms, feedback forms, file upload forms, or any form that should be collected by Netlify. Covers the data-netlify attribute, spam filtering, AJAX submissions,...” to “Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to...”. 119 additional added or edited lines are in the evidence.
Observed in instructions or declared skills. Runtime behavior has not been tested.
Product description
Guide for using Netlify Forms for HTML form handling. Use when adding contact forms, feedback forms, file upload forms, or any form that should be collected by Netlify. Covers the data-netlify attribute, spam filtering, AJAX submissions,...
Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to...
Skill instructions
Guide for using Netlify Forms for HTML form handling. Use when adding contact forms, feedback forms, file upload forms, or any form that should be collected by Netlify. Covers the data-netlify attribute, spam filtering, AJAX submissions,...
Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to...
Supporting files
[{"relative_path":"LICENSE.txt","size_in_bytes":10776},{"relative_path":"agents/openai.yaml","size_in_bytes":332},{"relative_path":"assets/netlify-small.svg","size_in_bytes":1291},{"relative_path":"assets/netlify.png","size_in_bytes":2686}]
[]
Compare saved observations
Download comparison JSONFull technical diff · 3 changed fields
changed /description
"Guide for using Netlify Forms for HTML form handling. Use when adding contact forms, feedback forms, file upload forms, or any form that should be collected by Netlify. Covers the data-netlify attribute, spam filtering, AJAX submissions, file uploads, notifications, and the submissions API."
"Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register."
changed /included_files
[
{
"relative_path": "LICENSE.txt",
"size_in_bytes": 10776
},
{
"relative_path": "agents/openai.yaml",
"size_in_bytes": 332
},
{
"relative_path": "assets/netlify-small.svg",
"size_in_bytes": 1291
},
{
"relative_path": "assets/netlify.png",
"size_in_bytes": 2686
}
][]
changed /skill_md_contents
"---\nname: netlify-forms\ndescription: Guide for using Netlify Forms for HTML form handling. Use when adding contact forms, feedback forms, file upload forms, or any form that should be collected by Netlify. Covers the data-netlify attribute, spam filtering, AJAX submissions, file uploads, notifications, and the submissions API.\n---\n\n# Netlify Forms\n\nNetlify Forms collects HTML form submissions without server-side code. Form detection must be enabled in the Netlify UI (Forms section).\n\n## Basic Setup\n\nAdd `data-netlify=\"true\"` and a unique `name` to the form:\n\n```html\n<form name=\"contact\" method=\"POST\" data-netlify=\"true\">\n <label>Name: <input type=\"text\" name=\"name\" /></label>\n <label>Email: <input type=\"email\" name=\"email\" /></label>\n <label>Message: <textarea name=\"message\"></textarea></label>\n <button type=\"submit\">Send</button>\n</form>\n```\n\nNetlify's build system detects the form and injects a hidden `form-name` input automatically. For a custom success page, add `action=\"/thank-you\"` to the form tag. Use paths without `.html` extension — Netlify serves `thank-you.html` at `/thank-you` by default, and the `.html` path returns 404.\n\n## JavaScript-Rendered Forms (React, Vue, SSR Frameworks)\n\nFor forms rendered by JavaScript frameworks (React, Vue, TanStack Start, Next.js, SvelteKit, Remix, Nuxt), Netlify's build parser cannot detect the form in static HTML. You MUST create a static HTML skeleton file for build-time form detection:\n\nCreate a static HTML file in `public/` (e.g. `public/__forms.html`) containing a hidden copy of each form:\n\n```html\n<!DOCTYPE html>\n<html>\n <body>\n <form name=\"contact\" data-netlify=\"true\" netlify-honeypot=\"bot-field\" hidden>\n <input type=\"hidden\" name=\"form-name\" value=\"contact\" />\n <input type=\"text\" name=\"name\" />\n <input type=\"email\" name=\"email\" />\n <textarea name=\"message\"></textarea>\n <input name=\"bot-field\" />\n </form>\n </body>\n</html>\n```\n\n**Rules:**\n- The form `name` must exactly match the `form-name` value used in your component's fetch call\n- Include every field your component submits — Netlify validates field names against the registered form\n- Without this file, Netlify cannot detect the form and submissions will silently fail\n\nYour component must also include a hidden `form-name` input:\n\n```jsx\n<form name=\"contact\" method=\"POST\" data-netlify=\"true\">\n <input type=\"hidden\" name=\"form-name\" value=\"contact\" />\n {/* ... fields ... */}\n</form>\n```\n\n## AJAX Submissions\n\n### Vanilla JavaScript\n\n```javascript\nconst form = document.querySelector(\"form\");\nform.addEventListener(\"submit\", async (e) => {\n e.preventDefault();\n const formData = new FormData(form);\n await fetch(\"/\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(formData).toString(),\n });\n});\n```\n\n> **SSR frameworks (TanStack Start, Next.js, SvelteKit, Remix, Nuxt):** The `fetch` URL must target the static skeleton\n> file path (e.g. `\"/__forms.html\"`), **not** `\"/\"`. In SSR apps, `fetch(\"/\")` is intercepted by the SSR catch-all\n> function and never reaches Netlify's form processing middleware. See the React example and troubleshooting section below.\n\n### React Example\n\n```tsx\nfunction ContactForm() {\n const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n const formData = new FormData(e.currentTarget);\n // For SSR apps, use the skeleton file path instead of \"/\" (e.g. \"/__forms.html\")\n const response = await fetch(\"/__forms.html\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(formData as any).toString(),\n });\n if (response.ok) {\n // Show success feedback\n }\n };\n\n return (\n <form name=\"contact\" method=\"POST\" data-netlify=\"true\" onSubmit={handleSubmit}>\n <input type=\"hidden\" name=\"form-name\" value=\"contact\" />\n <input type=\"text\" name=\"name\" placeholder=\"Name\" />\n <input type=\"email\" name=\"email\" placeholder=\"Email\" />\n <textarea name=\"message\" placeholder=\"Message\" />\n <button type=\"submit\">Send</button>\n </form>\n );\n}\n```\n\n> **SSR troubleshooting:** If form submissions appear to succeed (200 response) but nothing shows in the Netlify Forms\n> UI, the POST is likely being intercepted by the SSR function. Ensure `fetch` targets the skeleton file path (e.g.\n> `\"/__forms.html\"`), not `\"/\"`. The skeleton file path routes through the CDN origin where Netlify's form handler runs.\n\n## Spam Filtering\n\nNetlify uses Akismet automatically. Add a honeypot field for extra protection:\n\n```html\n<form name=\"contact\" method=\"POST\" netlify-honeypot=\"bot-field\" data-netlify=\"true\">\n <p style=\"display:none\">\n <label>Don't fill this out: <input name=\"bot-field\" /></label>\n </p>\n <!-- visible fields -->\n</form>\n```\n\nFor reCAPTCHA, add `data-netlify-recaptcha=\"true\"` to the form and include `<div data-netlify-recaptcha=\"true\"></div>` where the widget should appear.\n\n## File Uploads\n\n```html\n<form name=\"upload\" enctype=\"multipart/form-data\" data-netlify=\"true\">\n <input type=\"text\" name=\"name\" />\n <input type=\"file\" name=\"attachment\" />\n <button type=\"submit\">Upload</button>\n</form>\n```\n\nFor AJAX file uploads, use `FormData` directly — do **not** set `Content-Type` (the browser sets it with the correct boundary):\n\n```javascript\nawait fetch(\"/\", { method: \"POST\", body: new FormData(form) });\n```\n\n**Limits:** 8 MB max request size, 30-second timeout, one file per input field.\n\n## Notifications\n\nConfigure in the Netlify UI under **Project configuration > Notifications**:\n\n- **Email**: Auto-sends on submission. Add `<input type=\"hidden\" name=\"subject\" value=\"Contact form\" />` for custom subject lines.\n- **Slack**: Via Netlify App for Slack.\n- **Webhooks**: Trigger external services on submission.\n\n## Submissions API\n\nAccess submissions programmatically:\n\n```\nGET /api/v1/forms/{form_id}/submissions\nAuthorization: Bearer <PERSONAL_ACCESS_TOKEN>\n```\n\nKey endpoints:\n\n| Action | Method | Path |\n|---|---|---|\n| List forms | GET | `/api/v1/sites/{site_id}/forms` |\n| Get submissions | GET | `/api/v1/forms/{form_id}/submissions` |\n| Get spam | GET | `/api/v1/forms/{form_id}/submissions?state=spam` |\n| Delete submission | DELETE | `/api/v1/submissions/{id}` |\n""---\nname: netlify-forms\ndescription: Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register.\n---\n\n# Netlify Forms\n\nMark 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).\n\nPrerequisite: form detection must be enabled once in the Netlify UI (Forms > **Enable form detection**). Takes effect on the next deploy.\n\n## Static HTML form\n\n```html\n<form name=\"contact\" method=\"POST\" data-netlify=\"true\">\n <p><label>Your Name: <input type=\"text\" name=\"name\" /></label></p>\n <p><label>Your Email: <input type=\"email\" name=\"email\" /></label></p>\n <p><label>Message: <textarea name=\"message\"></textarea></label></p>\n <p><button type=\"submit\">Send</button></p>\n</form>\n```\n\n- `name` sets the form name in the UI and **must be unique per site**.\n- At deploy, Netlify strips the `data-netlify`/`netlify` attribute and injects `<input type=\"hidden\" name=\"form-name\" value=\"contact\" />`.\n- Add an `<input name=\"email\">` so the notification email's `Reply-to` is set to the submitter.\n\n## JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby)\n\nTwo required pieces:\n\n**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.\n\n```html\n<!-- public/__forms.html -->\n<form name=\"pizzaOrder\" data-netlify=\"true\" hidden>\n <input type=\"hidden\" name=\"form-name\" value=\"pizzaOrder\" />\n <input name=\"order\" type=\"text\" />\n</form>\n```\n\n**2. The rendered form** carries a matching hidden `form-name` input:\n\n```jsx\n<form name=\"pizzaOrder\" method=\"post\" data-netlify=\"true\" onSubmit={handleSubmit}>\n <input type=\"hidden\" name=\"form-name\" value=\"pizzaOrder\" />\n <input name=\"order\" type=\"text\" onChange={handleChange} />\n <input type=\"submit\" />\n</form>\n```\n\n**⚠️ SSR POST target:** In SSR apps, `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.\n\n**⚠️ Astro on-demand routes:** Routes with `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.\n\n**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\n\n## AJAX submission\n\n```js\nconst handleSubmit = event => {\n event.preventDefault();\n const formData = new FormData(event.target);\n fetch(\"/__forms.html\", { // static sites may POST to \"/\"; SSR must target the skeleton file\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(formData).toString()\n })\n .then(() => alert(\"Thank you for your submission\")) // or navigate(\"/thank-you\")\n .catch(error => alert(error));\n};\ndocument.querySelector(\"form\").addEventListener(\"submit\", handleSubmit);\n```\n\n- **Body MUST be URL-encoded. JSON is NOT supported.**\n- If the rendered form has no hidden `form-name` input, you MUST include a `form-name` field in the POST body.\n- The honeypot field name and `g-recaptcha-response` (if used) must be in the body — automatic with `FormData()`.\n\n## File uploads\n\nAdd `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).\n\n```js\ndocument.forms.fileForm.addEventListener(\"submit\", event => {\n event.preventDefault();\n fetch(\"/\", { body: new FormData(event.target), method: \"POST\" }) // no headers\n .then(() => { /* success */ });\n});\n```\n\nLimits: 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).\n\n## Custom success page\n\nAdd 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.\n\n```html\n<form name=\"contact\" action=\"/thank-you\" method=\"POST\" data-netlify=\"true\"></form>\n```\n\nCustom success *alert* is only possible via AJAX (substitute the redirect with your own logic).\n\n## Spam prevention\n\nAll submissions are filtered by Akismet. Passed → **Verified submissions**; flagged → **Spam submissions**. Honeypot/reCAPTCHA failures are rejected and appear in neither list.\n\n**Honeypot:** add `netlify-honeypot=\"bot-field\"` to the `<form>` and include a CSS-hidden field of that name. Any value entered → submission quietly rejected.\n\n```html\n<form name=\"contact\" method=\"POST\" netlify-honeypot=\"bot-field\" data-netlify=\"true\">\n <p class=\"hidden\"><label>Don’t fill this out: <input name=\"bot-field\" /></label></p>\n <!-- real fields -->\n</form>\n```\n\n**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.\n\n**Custom reCAPTCHA 2:** your own reCAPTCHA snippet + `data-netlify-recaptcha=\"true\"` on the `<form>`, plus env vars:\n- `SITE_RECAPTCHA_KEY` — site key (scopes: Builds + Runtime)\n- `SITE_RECAPTCHA_SECRET` — secret (scope: Runtime)\n\n## Email notifications & subject line\n\nDefault sender: `formresponses@netlify.com`. Set subject via a hidden `subject` input **or** the Netlify UI (Forms > Submission notifications) — **not both; the HTML value always overrides the UI.**\n\n```html\n<input type=\"hidden\" name=\"subject\" value=\"New lead from %{formName} (%{submissionId})\" />\n```\n\nVariables: `%{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.\n\nSet up notifications (email/webhook/Slack) in the UI: Forms > Submission notifications > **Add notification**.\n\n## Reading submissions via the API\n\nUse 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\n\n- **Page through results using the `Link` header** — code that reads only the first response silently drops the rest.\n- `listFormSubmissions` returns data from old/removed fields no longer shown in the UI.\n- Query spam with `?state=spam`.\n\n## Submission summary (field order matters)\n\nThe UI summary is derived from field **type**, not name:\n- **Title**: first non-hidden text `<input>` that isn't email-like (`type=\"email\"`, or name matching `email`/`mail`/`from`/`twitter`/`sender`); falls back to a field named `title` or `subject`.\n- **Body**: first `<textarea>`.\n\nField order in the HTML affects what appears in the summary.\n\n## Debugging missing submissions\n\n- **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.\n- Test submissions get flagged as spam: use a real email (not `test@test.com`), write full sentences, don't hammer from one IP.\n- No submissions at all: confirm form detection is enabled (Forms > Form detection) and redeploy.\n- SSR/JS forms silently failing: verify the static skeleton file exists with exactly-matching field names and that AJAX targets the skeleton file, not `/`.\n- Missing old-field data: the UI shows only fields from the last deployed form version. Mark old fields `hidden` instead of removing them to keep them visible; old data remains available via `listFormSubmissions`.\n\n## Constraints\n\n- Deleting a form is permanent: future submissions return `404`, past submissions become unavailable. Export CSV first.\n- Submitted code is sanitized (`<script>` → escaped entities).\n- For PII, export and delete data regularly.\n- Data is stored in Netlify's database, not accessible except via UI/API/CSV.\n\n<!-- Forms usage now at Forms > Usage; form detection at Forms > Form detection — UI paths updated per manifest commit a28cd46. -->\n\n<!-- system: agent-context/forms/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (forms)\n\nThese are org conventions and field-learned guardrails, not docs facts — they\nare merged into the rendered skill by ctx-gen and are never generated.\nExtracted from the previous hand-written netlify-forms skill; owned by the\nskills maintainer.\n\n1. In SSR apps (Next.js, Nuxt, SvelteKit, etc.), `fetch(\"/\")` is intercepted\n by the SSR catch-all function and never reaches Netlify's form processing.\n POST the AJAX submission to the static skeleton file itself (e.g.\n `/__forms.html`), not to an arbitrary path.\n2. Use only documented surfaces: do not curl `https://api.netlify.com/...`\n with an invented endpoint shape, and do not read tokens out of local CLI\n config files (`~/Library/Preferences/netlify/config.json`).\n3. When reading submissions via the API, page through results (`Link`\n header); code that reads only the first response silently drops the rest.\n4. For JS-rendered and SSR forms, always create the static skeleton file\n `public/__forms.html`: a hidden copy of each form with\n `data-netlify=\"true\"`, a hidden `form-name` input, and every field the\n component submits — names matching exactly (Netlify validates field names\n against the registered form). Without this file, submissions silently fail.\n5. Astro routes rendered on demand (`export const prerender = false`, or\n `output: \"server\"` routes) are never scanned at build time, so their forms\n are never registered. Put the form on a prerendered page or rely on the\n static skeleton file.\n6. A \"missing\" legitimate submission is usually an Akismet false positive:\n check the Spam list (or the API with `?state=spam`) and mark it verified.\n Do not build a custom recovery function or disable spam filtering as a\n first resort.\n7. For custom success pages, use extensionless `action` paths (`/thank-you`,\n not `/thank-you.html`) — Netlify serves `thank-you.html` at `/thank-you`\n and the `.html` path returns 404.\n"SKILL.md line diff
--- before +++ after @@ -1,172 +1,198 @@ --- name: netlify-forms -description: Guide for using Netlify Forms for HTML form handling. Use when adding contact forms, feedback forms, file upload forms, or any form that should be collected by Netlify. Covers the data-netlify attribute, spam filtering, AJAX submissions, file uploads, notifications, and the submissions API. +description: Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register. --- # Netlify Forms -Netlify Forms collects HTML form submissions without server-side code. Form detection must be enabled in the Netlify UI (Forms section). +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). -## Basic Setup +Prerequisite: form detection must be enabled once in the Netlify UI (Forms > **Enable form detection**). Takes effect on the next deploy. -Add `data-netlify="true"` and a unique `name` to the form: +## Static HTML form ```html <form name="contact" method="POST" data-netlify="true"> - <label>Name: <input type="text" name="name" /></label> - <label>Email: <input type="email" name="email" /></label> - <label>Message: <textarea name="message"></textarea></label> - <button type="submit">Send</button> + <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> ``` -Netlify's build system detects the form and injects a hidden `form-name` input automatically. For a custom success page, add `action="/thank-you"` to the form tag. Use paths without `.html` extension — Netlify serves `thank-you.html` at `/thank-you` by default, and the `.html` path returns 404. +- `name` sets the form name in the UI and **must be unique per site**. +- At deploy, Netlify strips the `data-netlify`/`netlify` attribute and injects `<input type="hidden" name="form-name" value="contact" />`. +- Add an `<input name="email">` so the notification email's `Reply-to` is set to the submitter. -## JavaScript-Rendered Forms (React, Vue, SSR Frameworks) +## JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby) -For forms rendered by JavaScript frameworks (React, Vue, TanStack Start, Next.js, SvelteKit, Remix, Nuxt), Netlify's build parser cannot detect the form in static HTML. You MUST create a static HTML skeleton file for build-time form detection: +Two required pieces: -Create a static HTML file in `public/` (e.g. `public/__forms.html`) containing a hidden copy of each form: +**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. ```html -<!DOCTYPE html> -<html> - <body> - <form name="contact" data-netlify="true" netlify-honeypot="bot-field" hidden> - <input type="hidden" name="form-name" value="contact" /> - <input type="text" name="name" /> - <input type="email" name="email" /> - <textarea name="message"></textarea> - <input name="bot-field" /> - </form> - </body> -</html> -``` - -**Rules:** -- The form `name` must exactly match the `form-name` value used in your component's fetch call -- Include every field your component submits — Netlify validates field names against the registered form -- Without this file, Netlify cannot detect the form and submissions will 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> +``` -Your component must also include a hidden `form-name` input: +**2. The rendered form** carries a matching hidden `form-name` input: ```jsx -<form name="contact" method="POST" data-netlify="true"> - <input type="hidden" name="form-name" value="contact" /> - {/* ... fields ... */} +<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> ``` -## AJAX Submissions +**⚠️ SSR POST target:** In SSR apps, `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. + +**⚠️ Astro on-demand routes:** Routes with `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 -### Vanilla JavaScript +## AJAX submission -```javascript -const form = document.querySelector("form"); -form.addEventListener("submit", async (e) => { - e.preventDefault(); - const formData = new FormData(form); - await fetch("/", { +```js +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(), - }); + 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-name` input, you MUST include a `form-name` field in the POST body. +- The honeypot field name and `g-recaptcha-response` (if used) must be in the body — automatic with `FormData()`. + +## 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). + +```js +document.forms.fileForm.addEventListener("submit", event => { + event.preventDefault(); + fetch("/", { body: new FormData(event.target), method: "POST" }) // no headers + .then(() => { /* success */ }); }); ``` -> **SSR frameworks (TanStack Start, Next.js, SvelteKit, Remix, Nuxt):** The `fetch` URL must target the static skeleton -> file path (e.g. `"/__forms.html"`), **not** `"/"`. In SSR apps, `fetch("/")` is intercepted by the SSR catch-all -> function and never reaches Netlify's form processing middleware. See the React example and troubleshooting section below. - -### React Example - -```tsx -function ContactForm() { - const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - // For SSR apps, use the skeleton file path instead of "/" (e.g. "/__forms.html") - const response = await fetch("/__forms.html", { - method: "POST", - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams(formData as any).toString(), - }); - if (response.ok) { - // Show success feedback - } - }; - - return ( - <form name="contact" method="POST" data-netlify="true" onSubmit={handleSubmit}> - <input type="hidden" name="form-name" value="contact" /> - <input type="text" name="name" placeholder="Name" /> - <input type="email" name="email" placeholder="Email" /> - <textarea name="message" placeholder="Message" /> - <button type="submit">Send</button> - </form> - ); -} -``` - -> **SSR troubleshooting:** If form submissions appear to succeed (200 response) but nothing shows in the Netlify Forms -> UI, the POST is likely being intercepted by the SSR function. Ensure `fetch` targets the skeleton file path (e.g. -> `"/__forms.html"`), not `"/"`. The skeleton file path routes through the CDN origin where Netlify's form handler runs. +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). -## Spam Filtering +## Custom success page -Netlify uses Akismet automatically. Add a honeypot field for extra protection: +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. ```html -<form name="contact" method="POST" netlify-honeypot="bot-field" data-netlify="true"> - <p style="display:none"> - <label>Don't fill this out: <input name="bot-field" /></label> - </p> - <!-- visible fields --> -</form> +<form name="contact" action="/thank-you" method="POST" data-netlify="true"></form> ``` -For reCAPTCHA, add `data-netlify-recaptcha="true"` to the form and include `<div data-netlify-recaptcha="true"></div>` where the widget should appear. +Custom success *alert* is only possible via AJAX (substitute the redirect with your own logic). -## File Uploads +## Spam prevention -```html -<form name="upload" enctype="multipart/form-data" data-netlify="true"> - <input type="text" name="name" /> - <input type="file" name="attachment" /> - <button type="submit">Upload</button> -</form> -``` +All submissions are filtered by Akismet. Passed → **Verified submissions**; flagged → **Spam submissions**. Honeypot/reCAPTCHA failures are rejected and appear in neither list. -For AJAX file uploads, use `FormData` directly — do **not** set `Content-Type` (the browser sets it with the correct boundary): +**Honeypot:** add `netlify-honeypot="bot-field"` to the `<form>` and include a CSS-hidden field of that name. Any value entered → submission quietly rejected. -```javascript -await fetch("/", { method: "POST", body: new FormData(form) }); +```html +<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> ``` -**Limits:** 8 MB max request size, 30-second timeout, one file per input field. +**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. -## Notifications +**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) -Configure in the Netlify UI under **Project configuration > Notifications**: +## Email notifications & subject line -- **Email**: Auto-sends on submission. Add `<input type="hidden" name="subject" value="Contact form" />` for custom subject lines. -- **Slack**: Via Netlify App for Slack. -- **Webhooks**: Trigger external services on submission. +Default sender: `formresponses@netlify.com`. Set subject via a hidden `subject` input **or** the Netlify UI (Forms > Submission notifications) — **not both; the HTML value always overrides the UI.** -## Submissions API +```html +<input type="hidden" name="subject" value="New lead from %{formName} (%{submissionId})" /> +``` -Access submissions programmatically: +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. -``` -GET /api/v1/forms/{form_id}/submissions -Authorization: Bearer <PERSONAL_ACCESS_TOKEN> -``` +Set up notifications (email/webhook/Slack) in the UI: Forms > Submission notifications > **Add notification**. -Key endpoints: +## Reading submissions via the API -| Action | Method | Path | -|---|---|---| -| List forms | GET | `/api/v1/sites/{site_id}/forms` | -| Get submissions | GET | `/api/v1/forms/{form_id}/submissions` | -| Get spam | GET | `/api/v1/forms/{form_id}/submissions?state=spam` | -| Delete submission | DELETE | `/api/v1/submissions/{id}` | +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 `Link` header** — code that reads only the first response silently drops the rest. +- `listFormSubmissions` returns 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 matching `email`/`mail`/`from`/`twitter`/`sender`); falls back to a field named `title` or `subject`. +- **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 `test@test.com`), write full sentences, don't hammer from one IP. +- No submissions at all: confirm form detection is enabled (Forms > Form detection) 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 `hidden` instead of removing them to keep them visible; old data remains available via `listFormSubmissions`. + +## 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. + +<!-- Forms usage now at Forms > Usage; form detection at Forms > Form detection — UI paths updated per manifest commit a28cd46. --> + +<!-- system: agent-context/forms/system.md — human-owned, merged by ctx-gen; edit system.md, not this section --> +# Netlify house rules (forms) + +These are org conventions and field-learned guardrails, not docs facts — they +are merged into the rendered skill by ctx-gen and are never generated. +Extracted from the previous hand-written netlify-forms skill; owned by the +skills maintainer. + +1. In SSR apps (Next.js, Nuxt, SvelteKit, etc.), `fetch("/")` is intercepted + by 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. +2. Use only documented surfaces: do not curl `https://api.netlify.com/...` + with an invented endpoint shape, and do not read tokens out of local CLI + config files (`~/Library/Preferences/netlify/config.json`). +3. When reading submissions via the API, page through results (`Link` + header); code that reads only the first response silently drops the rest. +4. For JS-rendered and SSR forms, always create the 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 — names matching exactly (Netlify validates field names + against the registered form). Without this file, submissions silently fail. +5. Astro routes rendered on demand (`export const prerender = false`, or + `output: "server"` routes) 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. +6. 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 a + first resort. +7. For custom success pages, use extensionless `action` paths (`/thank-you`, + not `/thank-you.html`) — Netlify serves `thank-you.html` at `/thank-you` + and the `.html` path returns 404.
Full snapshot data
{
"description": "Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register.",
"included_files": [],
"name": "netlify-forms",
"skill_md_contents": "---\nname: netlify-forms\ndescription: Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register.\n---\n\n# Netlify Forms\n\nMark 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).\n\nPrerequisite: form detection must be enabled once in the Netlify UI (Forms > **Enable form detection**). Takes effect on the next deploy.\n\n## Static HTML form\n\n```html\n<form name=\"contact\" method=\"POST\" data-netlify=\"true\">\n <p><label>Your Name: <input type=\"text\" name=\"name\" /></label></p>\n <p><label>Your Email: <input type=\"email\" name=\"email\" /></label></p>\n <p><label>Message: <textarea name=\"message\"></textarea></label></p>\n <p><button type=\"submit\">Send</button></p>\n</form>\n```\n\n- `name` sets the form name in the UI and **must be unique per site**.\n- At deploy, Netlify strips the `data-netlify`/`netlify` attribute and injects `<input type=\"hidden\" name=\"form-name\" value=\"contact\" />`.\n- Add an `<input name=\"email\">` so the notification email's `Reply-to` is set to the submitter.\n\n## JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby)\n\nTwo required pieces:\n\n**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.\n\n```html\n<!-- public/__forms.html -->\n<form name=\"pizzaOrder\" data-netlify=\"true\" hidden>\n <input type=\"hidden\" name=\"form-name\" value=\"pizzaOrder\" />\n <input name=\"order\" type=\"text\" />\n</form>\n```\n\n**2. The rendered form** carries a matching hidden `form-name` input:\n\n```jsx\n<form name=\"pizzaOrder\" method=\"post\" data-netlify=\"true\" onSubmit={handleSubmit}>\n <input type=\"hidden\" name=\"form-name\" value=\"pizzaOrder\" />\n <input name=\"order\" type=\"text\" onChange={handleChange} />\n <input type=\"submit\" />\n</form>\n```\n\n**⚠️ SSR POST target:** In SSR apps, `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.\n\n**⚠️ Astro on-demand routes:** Routes with `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.\n\n**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\n\n## AJAX submission\n\n```js\nconst handleSubmit = event => {\n event.preventDefault();\n const formData = new FormData(event.target);\n fetch(\"/__forms.html\", { // static sites may POST to \"/\"; SSR must target the skeleton file\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(formData).toString()\n })\n .then(() => alert(\"Thank you for your submission\")) // or navigate(\"/thank-you\")\n .catch(error => alert(error));\n};\ndocument.querySelector(\"form\").addEventListener(\"submit\", handleSubmit);\n```\n\n- **Body MUST be URL-encoded. JSON is NOT supported.**\n- If the rendered form has no hidden `form-name` input, you MUST include a `form-name` field in the POST body.\n- The honeypot field name and `g-recaptcha-response` (if used) must be in the body — automatic with `FormData()`.\n\n## File uploads\n\nAdd `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).\n\n```js\ndocument.forms.fileForm.addEventListener(\"submit\", event => {\n event.preventDefault();\n fetch(\"/\", { body: new FormData(event.target), method: \"POST\" }) // no headers\n .then(() => { /* success */ });\n});\n```\n\nLimits: 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).\n\n## Custom success page\n\nAdd 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.\n\n```html\n<form name=\"contact\" action=\"/thank-you\" method=\"POST\" data-netlify=\"true\"></form>\n```\n\nCustom success *alert* is only possible via AJAX (substitute the redirect with your own logic).\n\n## Spam prevention\n\nAll submissions are filtered by Akismet. Passed → **Verified submissions**; flagged → **Spam submissions**. Honeypot/reCAPTCHA failures are rejected and appear in neither list.\n\n**Honeypot:** add `netlify-honeypot=\"bot-field\"` to the `<form>` and include a CSS-hidden field of that name. Any value entered → submission quietly rejected.\n\n```html\n<form name=\"contact\" method=\"POST\" netlify-honeypot=\"bot-field\" data-netlify=\"true\">\n <p class=\"hidden\"><label>Don’t fill this out: <input name=\"bot-field\" /></label></p>\n <!-- real fields -->\n</form>\n```\n\n**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.\n\n**Custom reCAPTCHA 2:** your own reCAPTCHA snippet + `data-netlify-recaptcha=\"true\"` on the `<form>`, plus env vars:\n- `SITE_RECAPTCHA_KEY` — site key (scopes: Builds + Runtime)\n- `SITE_RECAPTCHA_SECRET` — secret (scope: Runtime)\n\n## Email notifications & subject line\n\nDefault sender: `formresponses@netlify.com`. Set subject via a hidden `subject` input **or** the Netlify UI (Forms > Submission notifications) — **not both; the HTML value always overrides the UI.**\n\n```html\n<input type=\"hidden\" name=\"subject\" value=\"New lead from %{formName} (%{submissionId})\" />\n```\n\nVariables: `%{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.\n\nSet up notifications (email/webhook/Slack) in the UI: Forms > Submission notifications > **Add notification**.\n\n## Reading submissions via the API\n\nUse 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\n\n- **Page through results using the `Link` header** — code that reads only the first response silently drops the rest.\n- `listFormSubmissions` returns data from old/removed fields no longer shown in the UI.\n- Query spam with `?state=spam`.\n\n## Submission summary (field order matters)\n\nThe UI summary is derived from field **type**, not name:\n- **Title**: first non-hidden text `<input>` that isn't email-like (`type=\"email\"`, or name matching `email`/`mail`/`from`/`twitter`/`sender`); falls back to a field named `title` or `subject`.\n- **Body**: first `<textarea>`.\n\nField order in the HTML affects what appears in the summary.\n\n## Debugging missing submissions\n\n- **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.\n- Test submissions get flagged as spam: use a real email (not `test@test.com`), write full sentences, don't hammer from one IP.\n- No submissions at all: confirm form detection is enabled (Forms > Form detection) and redeploy.\n- SSR/JS forms silently failing: verify the static skeleton file exists with exactly-matching field names and that AJAX targets the skeleton file, not `/`.\n- Missing old-field data: the UI shows only fields from the last deployed form version. Mark old fields `hidden` instead of removing them to keep them visible; old data remains available via `listFormSubmissions`.\n\n## Constraints\n\n- Deleting a form is permanent: future submissions return `404`, past submissions become unavailable. Export CSV first.\n- Submitted code is sanitized (`<script>` → escaped entities).\n- For PII, export and delete data regularly.\n- Data is stored in Netlify's database, not accessible except via UI/API/CSV.\n\n<!-- Forms usage now at Forms > Usage; form detection at Forms > Form detection — UI paths updated per manifest commit a28cd46. -->\n\n<!-- system: agent-context/forms/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (forms)\n\nThese are org conventions and field-learned guardrails, not docs facts — they\nare merged into the rendered skill by ctx-gen and are never generated.\nExtracted from the previous hand-written netlify-forms skill; owned by the\nskills maintainer.\n\n1. In SSR apps (Next.js, Nuxt, SvelteKit, etc.), `fetch(\"/\")` is intercepted\n by the SSR catch-all function and never reaches Netlify's form processing.\n POST the AJAX submission to the static skeleton file itself (e.g.\n `/__forms.html`), not to an arbitrary path.\n2. Use only documented surfaces: do not curl `https://api.netlify.com/...`\n with an invented endpoint shape, and do not read tokens out of local CLI\n config files (`~/Library/Preferences/netlify/config.json`).\n3. When reading submissions via the API, page through results (`Link`\n header); code that reads only the first response silently drops the rest.\n4. For JS-rendered and SSR forms, always create the static skeleton file\n `public/__forms.html`: a hidden copy of each form with\n `data-netlify=\"true\"`, a hidden `form-name` input, and every field the\n component submits — names matching exactly (Netlify validates field names\n against the registered form). Without this file, submissions silently fail.\n5. Astro routes rendered on demand (`export const prerender = false`, or\n `output: \"server\"` routes) are never scanned at build time, so their forms\n are never registered. Put the form on a prerendered page or rely on the\n static skeleton file.\n6. A \"missing\" legitimate submission is usually an Akismet false positive:\n check the Spam list (or the API with `?state=spam`) and mark it verified.\n Do not build a custom recovery function or disable spam filtering as a\n first resort.\n7. For custom success pages, use extensionless `action` paths (`/thank-you`,\n not `/thank-you.html`) — Netlify serves `thank-you.html` at `/thank-you`\n and the `.html` path returns 404.\n"
}SHA-256 of public snapshot: 17a4cd87b695ad6c11e599224f375ce3202b1832f7be0697149c5c040f0b0201