@netlify/agent-runner-cli 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin-local.js +160 -160
- package/dist/bin.js +199 -199
- package/dist/index.js +157 -157
- package/dist/skills/netlify-access-control/SKILL.md +136 -0
- package/dist/skills/netlify-access-control/references/two-layer-pattern.md +69 -0
- package/dist/skills/netlify-forms/SKILL.md +139 -283
- package/dist/skills/netlify-image-cdn/SKILL.md +116 -175
- package/dist/skills/netlify-image-cdn/references/user-uploads.md +156 -0
- package/package.json +2 -1
- package/scripts/build-skills.js +52 -0
- package/scripts/scenarios/install-skills.mjs +16 -6
|
@@ -1,358 +1,214 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: netlify-forms
|
|
3
|
-
description:
|
|
3
|
+
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Netlify Forms
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
automatically creates submission endpoints.
|
|
8
|
+
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).
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
Prerequisite: form detection must be enabled once in the Netlify UI (Forms > **Enable form detection**). Takes effect on the next deploy.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
is located in this skill's `scripts/` directory:
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
node scripts/enable.cjs
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Quick Start
|
|
21
|
-
|
|
22
|
-
Add `data-netlify="true"` or `netlify` attribute to any `<form>` tag:
|
|
12
|
+
## Static HTML form
|
|
23
13
|
|
|
24
14
|
```html
|
|
25
15
|
<form name="contact" method="POST" data-netlify="true">
|
|
26
|
-
<input type="text" name="name"
|
|
27
|
-
<input type="email" name="email"
|
|
28
|
-
<textarea name="message"></textarea>
|
|
29
|
-
<button type="submit">Send</button>
|
|
16
|
+
<p><label>Your Name: <input type="text" name="name" /></label></p>
|
|
17
|
+
<p><label>Your Email: <input type="email" name="email" /></label></p>
|
|
18
|
+
<p><label>Message: <textarea name="message"></textarea></label></p>
|
|
19
|
+
<p><button type="submit">Send</button></p>
|
|
30
20
|
</form>
|
|
31
21
|
```
|
|
32
22
|
|
|
33
|
-
**
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
- Method must be `POST`
|
|
37
|
-
- Netlify injects a hidden `form-name` field during build
|
|
38
|
-
|
|
39
|
-
## AJAX / JavaScript Submission
|
|
40
|
-
|
|
41
|
-
Submit forms without page reload:
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
const form = document.getElementById('contact-form')
|
|
45
|
-
|
|
46
|
-
form.addEventListener('submit', async (e) => {
|
|
47
|
-
e.preventDefault()
|
|
48
|
-
|
|
49
|
-
const response = await fetch('/', {
|
|
50
|
-
method: 'POST',
|
|
51
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
52
|
-
body: new URLSearchParams(new FormData(form)).toString(),
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
if (response.ok) {
|
|
56
|
-
// Handle success
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
> **SSR/SPA apps (React, Vue, TanStack Start, Next.js, SvelteKit, Remix, Nuxt):** You MUST create a static HTML skeleton
|
|
62
|
-
> file for build-time form detection. Without it, Netlify cannot detect the form during build and submissions will
|
|
63
|
-
> silently fail. Additionally, the `fetch` URL must target the skeleton file path (e.g. `/__forms.html`), **not** `/` —
|
|
64
|
-
> in SSR apps, `fetch('/')` is intercepted by the SSR function and never reaches Netlify's form processing middleware.
|
|
65
|
-
> See [JavaScript Frameworks](#javascript-frameworks-ssr--client-rendered-apps) below.
|
|
23
|
+
- `name` sets the form name in the UI and **must be unique per site**.
|
|
24
|
+
- At deploy, Netlify strips the `data-netlify`/`netlify` attribute and injects `<input type="hidden" name="form-name" value="contact" />`.
|
|
25
|
+
- Add an `<input name="email">` so the notification email's `Reply-to` is set to the submitter.
|
|
66
26
|
|
|
67
|
-
|
|
27
|
+
## JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby)
|
|
68
28
|
|
|
69
|
-
|
|
70
|
-
2. Include hidden `form-name` field in your HTML:
|
|
71
|
-
```html
|
|
72
|
-
<input type="hidden" name="form-name" value="contact" />
|
|
73
|
-
```
|
|
29
|
+
Two required pieces:
|
|
74
30
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Netlify's build bot cannot detect forms rendered client-side. For any SSR or SPA framework (React, Vue, TanStack Start,
|
|
78
|
-
Next.js, SvelteKit, Remix, Nuxt), you MUST:
|
|
79
|
-
|
|
80
|
-
1. **Create a static HTML skeleton** in `public/` for build-time form detection — this is the critical step. Without it,
|
|
81
|
-
Netlify never registers the form and submissions will silently fail or 404.
|
|
82
|
-
2. Submit via AJAX with `e.preventDefault()` — full-page POST will not work in SPAs
|
|
83
|
-
3. Include a hidden `form-name` field matching the form's `name` attribute
|
|
84
|
-
|
|
85
|
-
### Static Form Skeleton
|
|
86
|
-
|
|
87
|
-
Create a static HTML file (e.g. `public/__forms.html`) containing every form your app uses. The file name does not
|
|
88
|
-
matter — Netlify scans all HTML files in the build output:
|
|
31
|
+
**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.
|
|
89
32
|
|
|
90
33
|
```html
|
|
91
|
-
<!-- public/__forms.html
|
|
92
|
-
<
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<input name="name" />
|
|
97
|
-
<input name="email" />
|
|
98
|
-
<textarea name="message"></textarea>
|
|
99
|
-
<input name="bot-field" />
|
|
100
|
-
</form>
|
|
101
|
-
</body>
|
|
102
|
-
</html>
|
|
34
|
+
<!-- public/__forms.html -->
|
|
35
|
+
<form name="pizzaOrder" data-netlify="true" hidden>
|
|
36
|
+
<input type="hidden" name="form-name" value="pizzaOrder" />
|
|
37
|
+
<input name="order" type="text" />
|
|
38
|
+
</form>
|
|
103
39
|
```
|
|
104
40
|
|
|
105
|
-
**
|
|
106
|
-
|
|
107
|
-
- The form `name` attribute must exactly match the `form-name` value in your React/Vue component
|
|
108
|
-
- Include every field your component submits — Netlify validates field names against the registered form
|
|
109
|
-
- Add `netlify-honeypot="bot-field"` and a `bot-field` input for spam protection
|
|
110
|
-
|
|
111
|
-
### React Example
|
|
41
|
+
**2. The rendered form** carries a matching hidden `form-name` input:
|
|
112
42
|
|
|
113
43
|
```jsx
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// URL must point to the static skeleton file, not '/' — SSR catch-all intercepts '/'
|
|
120
|
-
await fetch('/__forms.html', {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
123
|
-
body: new URLSearchParams(formData).toString(),
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field" onSubmit={handleSubmit}>
|
|
129
|
-
{/* Hidden field required for AJAX */}
|
|
130
|
-
<input type="hidden" name="form-name" value="contact" />
|
|
131
|
-
<p style={{ display: 'none' }}>
|
|
132
|
-
<label>
|
|
133
|
-
Don't fill this out: <input name="bot-field" />
|
|
134
|
-
</label>
|
|
135
|
-
</p>
|
|
136
|
-
<input name="name" required />
|
|
137
|
-
<input name="email" type="email" required />
|
|
138
|
-
<textarea name="message" />
|
|
139
|
-
<button type="submit">Send</button>
|
|
140
|
-
</form>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## File Uploads
|
|
146
|
-
|
|
147
|
-
```html
|
|
148
|
-
<form name="upload" method="POST" data-netlify="true" enctype="multipart/form-data">
|
|
149
|
-
<input type="file" name="attachment" />
|
|
150
|
-
<button type="submit">Upload</button>
|
|
44
|
+
<form name="pizzaOrder" method="post" data-netlify="true" onSubmit={handleSubmit}>
|
|
45
|
+
<input type="hidden" name="form-name" value="pizzaOrder" />
|
|
46
|
+
<input name="order" type="text" onChange={handleChange} />
|
|
47
|
+
<input type="submit" />
|
|
151
48
|
</form>
|
|
152
49
|
```
|
|
153
50
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- Maximum request size: **8 MB**
|
|
157
|
-
- Timeout: **30 seconds**
|
|
158
|
-
- One file per input field (use multiple inputs for multiple files)
|
|
159
|
-
|
|
160
|
-
**Security:** For file uploads containing PII (personally identifiable information), use the
|
|
161
|
-
[Very Good Security (VGS)](https://www.netlify.com/integrations/very-good-security/) integration for additional
|
|
162
|
-
protection.
|
|
163
|
-
|
|
164
|
-
**AJAX file uploads:** Do NOT set Content-Type header - let browser set `multipart/form-data` with boundary:
|
|
51
|
+
**⚠️ 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.
|
|
165
52
|
|
|
166
|
-
|
|
167
|
-
// Correct - no Content-Type header
|
|
168
|
-
fetch('/', {
|
|
169
|
-
method: 'POST',
|
|
170
|
-
body: new FormData(form), // Browser sets correct Content-Type
|
|
171
|
-
})
|
|
172
|
-
```
|
|
53
|
+
**⚠️ 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.
|
|
173
54
|
|
|
174
|
-
|
|
55
|
+
**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
|
|
175
56
|
|
|
176
|
-
|
|
57
|
+
## AJAX submission
|
|
177
58
|
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
59
|
+
```js
|
|
60
|
+
const handleSubmit = event => {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
const formData = new FormData(event.target);
|
|
63
|
+
fetch("/__forms.html", { // static sites may POST to "/"; SSR must target the skeleton file
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
66
|
+
body: new URLSearchParams(formData).toString()
|
|
67
|
+
})
|
|
68
|
+
.then(() => alert("Thank you for your submission")) // or navigate("/thank-you")
|
|
69
|
+
.catch(error => alert(error));
|
|
70
|
+
};
|
|
71
|
+
document.querySelector("form").addEventListener("submit", handleSubmit);
|
|
189
72
|
```
|
|
190
73
|
|
|
191
|
-
|
|
74
|
+
- **Body MUST be URL-encoded. JSON is NOT supported.**
|
|
75
|
+
- If the rendered form has no hidden `form-name` input, you MUST include a `form-name` field in the POST body.
|
|
76
|
+
- The honeypot field name and `g-recaptcha-response` (if used) must be in the body — automatic with `FormData()`.
|
|
192
77
|
|
|
193
|
-
|
|
194
|
-
<form name="contact" method="POST" data-netlify="true" data-netlify-recaptcha="true">
|
|
195
|
-
<input name="name" required />
|
|
78
|
+
## File uploads
|
|
196
79
|
|
|
197
|
-
|
|
198
|
-
<div data-netlify-recaptcha="true"></div>
|
|
80
|
+
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).
|
|
199
81
|
|
|
200
|
-
|
|
201
|
-
|
|
82
|
+
```js
|
|
83
|
+
document.forms.fileForm.addEventListener("submit", event => {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
fetch("/", { body: new FormData(event.target), method: "POST" }) // no headers
|
|
86
|
+
.then(() => { /* success */ });
|
|
87
|
+
});
|
|
202
88
|
```
|
|
203
89
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
### Custom reCAPTCHA v2
|
|
207
|
-
|
|
208
|
-
Use your own reCAPTCHA 2 code with Netlify validation:
|
|
90
|
+
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).
|
|
209
91
|
|
|
210
|
-
|
|
211
|
-
your form.
|
|
212
|
-
2. Set environment variables in Netlify (UI, CLI, or API):
|
|
213
|
-
- `SITE_RECAPTCHA_KEY` — your reCAPTCHA site key (scopes: Builds + Runtime)
|
|
214
|
-
- `SITE_RECAPTCHA_SECRET` — your reCAPTCHA secret key (scope: Runtime)
|
|
215
|
-
3. Add `data-netlify-recaptcha="true"` to your `<form>` tag.
|
|
92
|
+
## Custom success page
|
|
216
93
|
|
|
217
|
-
Netlify
|
|
218
|
-
|
|
219
|
-
**For AJAX with reCAPTCHA:** Include `g-recaptcha-response` in POST body (automatic if using `FormData()`).
|
|
220
|
-
|
|
221
|
-
## Success Redirects
|
|
94
|
+
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.
|
|
222
95
|
|
|
223
96
|
```html
|
|
224
|
-
|
|
225
|
-
<form name="contact" method="POST" data-netlify="true" action="/thank-you"></form>
|
|
97
|
+
<form name="contact" action="/thank-you" method="POST" data-netlify="true"></form>
|
|
226
98
|
```
|
|
227
99
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
- Start with `/`
|
|
231
|
-
- Be relative to site root
|
|
232
|
-
- Point to an existing page
|
|
233
|
-
|
|
234
|
-
> **Pretty URLs:** Netlify serves `thank-you.html` at `/thank-you` by default. Use `action="/thank-you"`, not
|
|
235
|
-
> `action="/thank-you.html"` — the `.html` path returns 404.
|
|
236
|
-
|
|
237
|
-
## Notifications
|
|
238
|
-
|
|
239
|
-
### Email Notifications
|
|
240
|
-
|
|
241
|
-
Configure in Netlify UI: **Project configuration > Notifications > Emails and webhooks > Form submission notifications**
|
|
100
|
+
Custom success *alert* is only possible via AJAX (substitute the redirect with your own logic).
|
|
242
101
|
|
|
243
|
-
|
|
244
|
-
- Custom subject line:
|
|
245
|
-
```html
|
|
246
|
-
<input type="hidden" name="subject" value="New inquiry from %{formName}" />
|
|
247
|
-
```
|
|
248
|
-
- Available variables: `%{formName}`, `%{siteName}`, `%{submissionId}`
|
|
249
|
-
- For forms created before May 5, 2023: remove `[Netlify]` prefix from subject by adding `data-remove-prefix`:
|
|
250
|
-
```html
|
|
251
|
-
<input type="hidden" name="subject" data-remove-prefix value="Sales inquiry" />
|
|
252
|
-
```
|
|
102
|
+
## Spam prevention
|
|
253
103
|
|
|
254
|
-
|
|
104
|
+
All submissions are filtered by Akismet. Passed → **Verified submissions**; flagged → **Spam submissions**. Honeypot/reCAPTCHA failures are rejected and appear in neither list.
|
|
255
105
|
|
|
256
|
-
|
|
106
|
+
**Honeypot:** add `netlify-honeypot="bot-field"` to the `<form>` and include a CSS-hidden field of that name. Any value entered → submission quietly rejected.
|
|
257
107
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
// netlify/functions/submission-created.mts
|
|
266
|
-
import type { Context } from '@netlify/functions'
|
|
267
|
-
|
|
268
|
-
interface FormPayload {
|
|
269
|
-
form_name: string
|
|
270
|
-
data: Record<string, string>
|
|
271
|
-
created_at: string
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export default async (req: Request, context: Context) => {
|
|
275
|
-
const { payload } = (await req.json()) as { payload: FormPayload }
|
|
276
|
-
|
|
277
|
-
console.log('Form:', payload.form_name)
|
|
278
|
-
console.log('Data:', payload.data)
|
|
279
|
-
|
|
280
|
-
// Process submission (send to CRM, Slack, etc.)
|
|
281
|
-
|
|
282
|
-
return new Response('OK')
|
|
283
|
-
}
|
|
108
|
+
```html
|
|
109
|
+
<form name="contact" method="POST" netlify-honeypot="bot-field" data-netlify="true">
|
|
110
|
+
<p class="hidden"><label>Don’t fill this out: <input name="bot-field" /></label></p>
|
|
111
|
+
<!-- real fields -->
|
|
112
|
+
</form>
|
|
284
113
|
```
|
|
285
114
|
|
|
286
|
-
**
|
|
287
|
-
|
|
288
|
-
## Limits
|
|
115
|
+
**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.
|
|
289
116
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
| File Storage | 10 MB/month | Scalable |
|
|
294
|
-
| Request Size | 8 MB | 8 MB |
|
|
295
|
-
| Timeout | 30 seconds | 30 seconds |
|
|
117
|
+
**Custom reCAPTCHA 2:** your own reCAPTCHA snippet + `data-netlify-recaptcha="true"` on the `<form>`, plus env vars:
|
|
118
|
+
- `SITE_RECAPTCHA_KEY` — site key (scopes: Builds + Runtime)
|
|
119
|
+
- `SITE_RECAPTCHA_SECRET` — secret (scope: Runtime)
|
|
296
120
|
|
|
297
|
-
##
|
|
121
|
+
## Email notifications & subject line
|
|
298
122
|
|
|
299
|
-
|
|
123
|
+
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.**
|
|
300
124
|
|
|
301
|
-
|
|
125
|
+
```html
|
|
126
|
+
<input type="hidden" name="subject" value="New lead from %{formName} (%{submissionId})" />
|
|
127
|
+
```
|
|
302
128
|
|
|
303
|
-
|
|
304
|
-
2. Verify `form-name` hidden input is present
|
|
305
|
-
3. Check form has `name` attribute
|
|
129
|
+
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.
|
|
306
130
|
|
|
307
|
-
|
|
131
|
+
Set up notifications (email/webhook/Slack) in the UI: Forms > Submission notifications > **Add notification**.
|
|
308
132
|
|
|
309
|
-
|
|
133
|
+
## Reading submissions via the API
|
|
310
134
|
|
|
311
|
-
|
|
312
|
-
2. Avoid test values like "test@test.com" or "asdf" — use real email and full sentences
|
|
313
|
-
3. Verify form was included in the latest deploy
|
|
135
|
+
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
|
|
314
136
|
|
|
315
|
-
|
|
137
|
+
- **Page through results using the `Link` header** — code that reads only the first response silently drops the rest.
|
|
138
|
+
- `listFormSubmissions` returns data from old/removed fields no longer shown in the UI.
|
|
139
|
+
- Query spam with `?state=spam`.
|
|
316
140
|
|
|
317
|
-
|
|
141
|
+
## Submission summary (field order matters)
|
|
318
142
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
143
|
+
The UI summary is derived from field **type**, not name:
|
|
144
|
+
- **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`.
|
|
145
|
+
- **Body**: first `<textarea>`.
|
|
322
146
|
|
|
323
|
-
|
|
147
|
+
Field order in the HTML affects what appears in the summary.
|
|
324
148
|
|
|
325
|
-
|
|
149
|
+
## Debugging missing submissions
|
|
326
150
|
|
|
327
|
-
|
|
328
|
-
|
|
151
|
+
- **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.
|
|
152
|
+
- Test submissions get flagged as spam: use a real email (not `test@test.com`), write full sentences, don't hammer from one IP.
|
|
153
|
+
- No submissions at all: confirm form detection is enabled (Forms > Form detection) and redeploy.
|
|
154
|
+
- SSR/JS forms silently failing: verify the static skeleton file exists with exactly-matching field names and that AJAX targets the skeleton file, not `/`.
|
|
155
|
+
- 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`.
|
|
156
|
+
- Still stuck: Netlify Support Guide on how to debug your form — https://answers.netlify.com/t/common-issue-how-to-debug-your-form/92
|
|
329
157
|
|
|
330
|
-
|
|
331
|
-
function (TanStack Start, Next.js, SvelteKit, Remix, Nuxt) — the SSR handler renders a 200 HTML page, so `fetch()`
|
|
332
|
-
reports success, but the request never reaches Netlify's form processing middleware.
|
|
158
|
+
## Constraints
|
|
333
159
|
|
|
334
|
-
|
|
160
|
+
- Deleting a form is permanent: future submissions return `404`, past submissions become unavailable. Export CSV first.
|
|
161
|
+
- Submitted code is sanitized (`<script>` → escaped entities).
|
|
162
|
+
- For PII, export and delete data regularly.
|
|
163
|
+
- Data is stored in Netlify's database, not accessible except via UI/API/CSV.
|
|
335
164
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
2. Change `fetch('/')` to `fetch('/__forms.html')` (or whatever path your skeleton file is at) so the request routes
|
|
339
|
-
through the CDN origin where Netlify's `formsHandler` runs.
|
|
165
|
+
<!-- system: agent-context/forms/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->
|
|
166
|
+
# Netlify house rules (forms)
|
|
340
167
|
|
|
341
|
-
|
|
168
|
+
These are org conventions and field-learned guardrails, not docs facts — they
|
|
169
|
+
are merged into the rendered skill by ctx-gen and are never generated.
|
|
170
|
+
Extracted from the previous hand-written netlify-forms skill; owned by the
|
|
171
|
+
skills maintainer.
|
|
342
172
|
|
|
343
|
-
|
|
173
|
+
1. In SSR apps (Next.js, Nuxt, SvelteKit, etc.), `fetch("/")` is intercepted
|
|
174
|
+
by the SSR catch-all function and never reaches Netlify's form processing.
|
|
175
|
+
POST the AJAX submission to the static skeleton file itself (e.g.
|
|
176
|
+
`/__forms.html`), not to an arbitrary path.
|
|
177
|
+
2. Use only documented surfaces: do not curl `https://api.netlify.com/...`
|
|
178
|
+
with an invented endpoint shape, and do not read tokens out of local CLI
|
|
179
|
+
config files (`~/Library/Preferences/netlify/config.json`).
|
|
180
|
+
3. When reading submissions via the API, page through results (`Link`
|
|
181
|
+
header); code that reads only the first response silently drops the rest.
|
|
182
|
+
4. For JS-rendered and SSR forms, always create the static skeleton file
|
|
183
|
+
`public/__forms.html`: a hidden copy of each form with
|
|
184
|
+
`data-netlify="true"`, a hidden `form-name` input, and every field the
|
|
185
|
+
component submits — names matching exactly (Netlify validates field names
|
|
186
|
+
against the registered form). Without this file, submissions silently fail.
|
|
187
|
+
5. Astro routes rendered on demand (`export const prerender = false`, or
|
|
188
|
+
`output: "server"` routes) are never scanned at build time, so their forms
|
|
189
|
+
are never registered. Put the form on a prerendered page or rely on the
|
|
190
|
+
static skeleton file.
|
|
191
|
+
6. A "missing" legitimate submission is usually an Akismet false positive:
|
|
192
|
+
check the Spam list (or the API with `?state=spam`) and mark it verified.
|
|
193
|
+
Do not build a custom recovery function or disable spam filtering as a
|
|
194
|
+
first resort.
|
|
195
|
+
7. For custom success pages, use extensionless `action` paths (`/thank-you`,
|
|
196
|
+
not `/thank-you.html`) — Netlify serves `thank-you.html` at `/thank-you`
|
|
197
|
+
and the `.html` path returns 404.
|
|
344
198
|
|
|
345
|
-
1. Total request size under 8 MB
|
|
346
|
-
2. Not setting Content-Type header manually
|
|
347
|
-
3. Using `enctype="multipart/form-data"` on form
|
|
348
199
|
|
|
349
|
-
##
|
|
200
|
+
## Additional requirements
|
|
350
201
|
|
|
351
|
-
|
|
202
|
+
After implementing Netlify Forms, you MUST run `scripts/enable.cjs` from this skill using Node. Use the absolute path to
|
|
203
|
+
the script and keep the project's directory as your working directory:
|
|
352
204
|
|
|
353
205
|
```bash
|
|
354
|
-
|
|
355
|
-
https://api.netlify.com/api/v1/forms/{form_id}/submissions
|
|
206
|
+
node /absolute/path/to/netlify-forms/scripts/enable.cjs
|
|
356
207
|
```
|
|
357
208
|
|
|
358
|
-
|
|
209
|
+
Replace the example path with the installed location of this skill. The script creates `.netlify/features/netlify-forms`
|
|
210
|
+
in the project's repository. The deployment workflow uses that marker to request form detection enablement before
|
|
211
|
+
deploying.
|
|
212
|
+
|
|
213
|
+
In this environment, this script replaces the manual UI enablement prerequisite above. Do not ask the user to enable
|
|
214
|
+
form detection in the Netlify UI; run the script after implementing the form.
|