codex/skills/netlify-forms/SKILL.md
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.
npx skillsauth add netlify/context-and-tools netlify-formsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Netlify Forms collects HTML form submissions without server-side code. Form detection must be enabled in the Netlify UI (Forms section).
Add data-netlify="true" and a unique name to the form:
<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>
</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.
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:
Create a static HTML file in public/ (e.g. public/__forms.html) containing a hidden copy of each form:
<!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:
name must exactly match the form-name value used in your component's fetch callYour component must also include a hidden form-name input:
<form name="contact" method="POST" data-netlify="true">
<input type="hidden" name="form-name" value="contact" />
{/* ... fields ... */}
</form>
const form = document.querySelector("form");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(form);
await fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(formData).toString(),
});
});
SSR frameworks (TanStack Start, Next.js, SvelteKit, Remix, Nuxt): The
fetchURL 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.
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
fetchtargets the skeleton file path (e.g."/__forms.html"), not"/". The skeleton file path routes through the CDN origin where Netlify's form handler runs.
Netlify uses Akismet automatically. Add a honeypot field for extra protection:
<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>
For reCAPTCHA, add data-netlify-recaptcha="true" to the form and include <div data-netlify-recaptcha="true"></div> where the widget should appear.
<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>
For AJAX file uploads, use FormData directly — do not set Content-Type (the browser sets it with the correct boundary):
await fetch("/", { method: "POST", body: new FormData(form) });
Limits: 8 MB max request size, 30-second timeout, one file per input field.
Configure in the Netlify UI under Project configuration > Notifications:
<input type="hidden" name="subject" value="Contact form" /> for custom subject lines.Access submissions programmatically:
GET /api/v1/forms/{form_id}/submissions
Authorization: Bearer <PERSONAL_ACCESS_TOKEN>
Key endpoints:
| 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} |
devops
Guide for using Netlify Database — the GA managed Postgres product built into Netlify. Use when a project needs any kind of dynamic, structured, or relational data. Covers provisioning via @netlify/database, Drizzle ORM (@beta) setup, migrations, preview branching, and safe production data handling. Blobs is only for file/asset storage — any dynamic data belongs in the database.
devops
Guide for using Netlify Database — the GA managed Postgres product built into Netlify. Use when a project needs any kind of dynamic, structured, or relational data. Covers provisioning via @netlify/database, Drizzle ORM (@beta) setup, migrations, preview branching, and safe production data handling. Blobs is only for file/asset storage — any dynamic data belongs in the database.
development
Reference for Netlify AI Gateway — the managed proxy that routes calls to OpenAI, Anthropic, and Google Gemini SDKs without provider API keys. Use this skill any time the user wants to add AI on a Netlify site (chat, completion, reasoning, image generation, image-to-image edit/stylize), choose or change a model, wire up the OpenAI / Anthropic / @google/genai SDK, decide which provider to use for an image-gen feature (it's Gemini-only on the gateway), or debug "model not found" / "API key missing" against the gateway. Required reading before pinning a model — the gateway exposes a curated subset, not every provider model.
development
Guide for using Netlify Image CDN for image optimization and transformation. Use when serving optimized images, creating responsive image markup, setting up user-uploaded image pipelines, or configuring image transformations. Covers the /.netlify/images endpoint, query parameters, remote image allowlisting, clean URL rewrites, and composing uploads with Functions + Blobs.