netlify-forms-tanstack
$
npx mdskill add netlify/swar-templates/netlify-forms-tanstackCreate static dummy forms to enable Netlify detection for React-rendered submissions.
- Solves silent form submission failures on TanStack Start sites
- Integrates with Netlify build-time scanning and public folder structure
- Decides form handling by generating hidden HTML mirroring React fields
- Delivers working form endpoints that intercept POST requests correctly
SKILL.md
.github/skills/netlify-forms-tanstackView on GitHub ↗
---
name: netlify-forms-tanstack
description: Handle Netlify Forms in TanStack Start. Use when implementing contact forms, signup forms, or any form submission handling on Netlify-hosted TanStack Start sites.
license: Apache-2.0
metadata:
author: netlify
version: "1.0"
---
# Netlify Forms — TanStack Start Addendum
> **Base skill**: General Netlify Forms usage (HTML setup, spam filtering, honeypot fields, notifications) is covered by the `netlify-forms` skill in the agent-runner base skills. This skill covers **only** the TanStack Start-specific requirement on top of that.
---
## The Problem: Netlify Can't Detect React-Rendered Forms
TanStack Start renders pages via React on the client side. Netlify's build-time form detection works by scanning the **static HTML output** of your build. Because TanStack Start's forms live inside React components, Netlify never sees them during the build — so the form is never registered and submissions will silently fail. Without build-time detection, POSTs containing form data pass through to the SSR function instead of being intercepted by Netlify's form processing.
---
## The Solution: Dummy Static Form in `./public/`
Place a minimal static HTML file in `./public/` containing a hidden form that mirrors your React form's fields. Netlify scans `public/` at build time, registers the form name, and starts accepting submissions to it.
### `public/contact-form.html`
```html
<!DOCTYPE html>
<html>
<body>
<!-- This file exists only so Netlify registers the form at build time. -->
<!-- It is never shown to users. -->
<form name="contact" netlify netlify-honeypot="bot-field" hidden>
<input type="text" name="name" />
<input type="email" name="email" />
<textarea name="message"></textarea>
</form>
</body>
</html>
```
**Rules:**
- `name="contact"` must exactly match the `name` attribute in your React component's fetch call.
- Include every field your React form submits — Netlify validates field names against the registered form.
- Add `netlify-honeypot="bot-field"` here if your React form uses a honeypot field.
---
## The React Component
Submit via AJAX using `fetch` with `application/x-www-form-urlencoded` encoding. Do **not** use a plain `<form action="/">` — that causes a full-page reload and breaks TanStack Router's client-side navigation.
```tsx
import { useState } from 'react'
function encode(data: Record<string, string>) {
return Object.entries(data)
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join('&')
}
export function ContactForm() {
const [fields, setFields] = useState({ name: '', email: '', message: '' })
const [submitted, setSubmitted] = useState(false)
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => setFields({ ...fields, [e.target.name]: e.target.value })
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await fetch('/contact-form.html', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: encode({ 'form-name': 'contact', ...fields }),
})
setSubmitted(true)
}
if (submitted) return <p>Thanks! We'll be in touch.</p>
return (
<form onSubmit={handleSubmit}>
{/* Required hidden field — tells Netlify which registered form this maps to */}
<input type="hidden" name="form-name" value="contact" />
<label>
Name
<input type="text" name="name" value={fields.name} onChange={handleChange} required />
</label>
<label>
Email
<input type="email" name="email" value={fields.email} onChange={handleChange} required />
</label>
<label>
Message
<textarea name="message" value={fields.message} onChange={handleChange} required />
</label>
<button type="submit">Send</button>
</form>
)
}
```
**Key points:**
- The hidden `<input name="form-name" value="contact" />` is mandatory — it tells Netlify which form to associate the submission with.
- `e.preventDefault()` prevents full-page navigation.
- The `fetch` body uses URL-encoded encoding, not JSON — Netlify Forms requires this format.
> **SSR interception warning:** In TanStack Start (and other SSR frameworks), `fetch('/')` will be intercepted by the SSR catch-all function and never reach Netlify's form processing middleware. The `fetch` URL **must** point to the static skeleton file path (e.g. `/contact-form.html`), not `/`. This ensures the request routes through the CDN origin where Netlify's `formsHandler` can process it.
---
## Checklist
- [ ] **Static HTML skeleton exists** in `public/` (e.g. `public/__forms.html`) with `data-netlify="true"` and all field names — this is the most critical step
- [ ] Form `name` in the static HTML exactly matches `form-name` value in the React component
- [ ] Every field in the React form is also in the static HTML skeleton
- [ ] React component has `<input type="hidden" name="form-name" value="..." />`
- [ ] `fetch` posts to the static skeleton file path (NOT `/`) with `Content-Type: application/x-www-form-urlencoded`
- [ ] Deployed to Netlify (forms don't work in local dev — test on a deploy preview)
More from netlify/swar-templates
- content-collectionsUse content-collections for type-safe content management with markdown files. Use when building blogs, documentation sites, or any content-driven pages with frontmatter and markdown.
- netlify-identity-tanstack-startUpgrade a stock TanStack Start project to use Netlify Identity for authentication via the @netlify/identity package. Use this skill whenever the user wants to add Netlify Identity auth to a TanStack Start app, integrate login/signup into TanStack Start, protect routes or server functions with Netlify Identity, add role-based access control, or wire up Netlify Identity webhooks. Also use when the user mentions '@netlify/identity', 'nf_jwt', or asks about auth for TanStack Start on Netlify. Covers SSR pages, SPA pages, API routes, server functions, middleware, route guards, role-based access, identity webhooks, and client-side auth state.
- tanstack-start-api-routesCreate API routes (server routes) in TanStack Start for handling HTTP requests. Use when building REST APIs, webhooks, or any HTTP endpoint that returns data rather than rendering a page.
- tanstack-start-loadersLoad data for TanStack Start routes using beforeLoad and loader functions. Use when fetching data for pages, implementing route guards, or setting up route context. IMPORTANT - Loaders should call server functions for data access.
- tanstack-start-project-setupSet up and configure TanStack Start projects. Use when creating new projects, configuring the router, setting up TanStack Query integration, or configuring build settings.
- tanstack-start-routesCreate and manage routes in TanStack Start using file-based routing. Use when adding new pages, configuring layouts, setting up nested routes, or working with route parameters.
- tanstack-start-server-functionsCreate server functions in TanStack Start for server-side logic callable from anywhere. Use for database access, API calls with secrets, mutations, server-only code, or when you must use .inputValidator(...) for createServerFn inputs.
- tanstack-start-typesafe-routingImplement type-safe navigation and links in TanStack Start. Use when creating links, navigating programmatically, working with search params, or accessing route parameters with full TypeScript support.