# Ajouter un formulaire de contact sur Netlify sans Netlify Forms

> Un formulaire de contact sur Netlify sans Netlify Forms : pas de détection au build, envois JSON depuis React, et un formulaire à part pour les deploy previews.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/contact-form-static-site/netlify

Laissez `data-netlify` hors du formulaire et faites pointer son `action` vers votre endpoint Ikiform ; Netlify sert alors la page sans jamais toucher à l'envoi. Les formulaires React et Vue n'ont besoin d'aucune copie HTML masquée, et peuvent envoyer du JSON. Définissez un id de formulaire différent pour les deploy previews dans `netlify.toml`, car les hôtes de préproduction ne correspondent pas à votre site dans **Allowed domains**.

## Ce qui change sur Netlify

Netlify est le seul grand hébergeur statique avec une fonctionnalité de formulaires intégrée. Netlify Forms lit votre HTML déployé après chaque build, trouve les formulaires marqués `data-netlify` ou `netlify`, et accepte les envois vers votre propre site pour ces formulaires. Tout autre envoi vers une page statique échoue, et sur Netlify il échoue avec `404`, pas `405` :

`Terminal`:

```bash
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
```

`Réponse`:

```http
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0
```

Ce `404` pousse à chercher une page manquante qui ne l'est pas. Il signifie qu'aucun formulaire détecté ne correspondait à l'envoi. Savoir s'il faut utiliser Netlify Forms est une autre question, et le [comparatif Ikiform vs Netlify Forms](https://ikiform.com/alternatives/netlify-forms) y répond honnêtement : si votre site restera toujours sur Netlify et que le formulaire est en HTML simple, Netlify Forms demande moins de travail. Cette page couvre les autres cas.

| Votre situation | Pourquoi Netlify Forms gêne |
| --- | --- |
| Le formulaire est rendu par React, Vue ou Svelte | Le build analyse le HTML statique : un formulaire rendu côté client a donc besoin d'un double masqué que Netlify puisse trouver |
| Vous voulez envoyer du JSON depuis `fetch` | Netlify Forms n'accepte que des corps urlencoded et multipart |
| Le site pourrait changer d'hébergeur | Le formulaire envoie vers votre propre domaine : il ne fonctionne donc que tant que Netlify sert ce domaine |
| Les messages doivent arriver à un webhook que vous vérifiez | Ikiform signe chaque livraison avec les en-têtes Standard Webhooks et la réessaie |

Schéma : Netlify Forms et Ikiform côte à côte sur le même site Netlify

1. Avec Netlify Forms, Netlify analyse le HTML de chaque déploiement à la recherche des formulaires marqués data-netlify.
2. Un formulaire trouvé accepte les envois vers votre propre site, et Netlify les stocke.
3. Un formulaire non trouvé, comme un formulaire rendu en JavaScript sans copie masquée, reçoit 404 à chaque envoi.
4. Avec Ikiform, rien n'a besoin d'être trouvé au build.
5. Le formulaire envoie directement à ikiform.com, en HTML ou en JSON, et Ikiform le stocke et vous l'envoie par e-mail.

Les deux peuvent cohabiter sur un même site. Un formulaire avec data-netlify va chez Netlify ; un formulaire dont l'action est un endpoint Ikiform va chez Ikiform.

## Sortir un formulaire de Netlify Forms

- Retirez `data-netlify` ou `netlify`, `netlify-honeypot` et `data-netlify-recaptcha` du formulaire, ainsi que tout input masqué `form-name`. Si vous le laissez, `form-name` est stocké comme un champ ordinaire.
- Supprimez la copie statique masquée d'un formulaire JavaScript, comme celle ci-dessous. Netlify n'en avait besoin que pour détecter le formulaire.
- Réglez l'`action`, ou l'URL du `fetch`, sur votre endpoint Ikiform. L'ancien `action="/thanks"` passe dans le réglage **Redirect after submit** du formulaire.
- Téléchargez d'abord les soumissions passées depuis la page **Forms** de Netlify. Elles ne migrent pas toutes seules.

`index.html, à supprimer`:

```html
<form name="contact" netlify hidden>
  <input name="name" />
  <input name="email" />
  <textarea name="message"></textarea>
</form>
```

Laissez la **détection des formulaires** activée si d'autres formulaires du site utilisent encore Netlify Forms. La documentation de Netlify indique que la désactivation n'est prévue que pour les sites qui n'utilisent pas Netlify Forms. Une fois que plus rien ne l'utilise, la désactiver évite une analyse à chaque déploiement.

### Ce qui remplace les réglages anti-spam de Netlify

Netlify passe les soumissions dans Akismet et propose `netlify-honeypot` et reCAPTCHA. Sur Ikiform, le honeypot continue de fonctionner si vous réglez le **Honeypot field** du formulaire sur le nom que vous utilisez déjà, comme `bot-field`. Le score de spam s'applique à chaque envoi sans configuration, et **Spam check** réglé sur **Ikiform** remplace reCAPTCHA par Cloudflare Turnstile, sans clés. Retirez la `div` reCAPTCHA lors de la bascule.

## Un formulaire React qui envoie du JSON

Sans Netlify Forms dans les pattes, un formulaire rendu côté client n'a pas besoin de double, et il peut envoyer du JSON, ce qui préserve les types et permet de nettoyer les valeurs au préalable. Celui-ci enregistre aussi quel contexte Netlify a construit la page :

`src/ContactForm.tsx`:

```tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = `https://ikiform.com/f/${import.meta.env.VITE_IKIFORM_FORM_ID}`;

interface Message {
  name: string;
  email: string;
  message: string;
  context: string;
}

export function ContactForm() {
  const [sent, setSent] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const body: Message & Record<string, string> = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      context: import.meta.env.VITE_CONTEXT ?? "local",
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
    setError("");
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    }).catch(() => null);
    const result = await response?.json().catch(() => null);
    if (result?.ok) return setSent(true);
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    setError(Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.");
  }

  if (sent) return <p role="status">Got it. I'll answer from my own inbox, usually the same day.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} minLength={10} required />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button>Send</button>
    </form>
  );
}
```

- `Content-Type: application/json` indique à Ikiform d'analyser du JSON ; `Accept: application/json` demande une réponse en JSON.
- `VITE_CONTEXT` est un nom d'exemple. Netlify définit `CONTEXT` pendant le build ; exposez-le à Vite dans votre commande de build, ou supprimez le champ.
- Le honeypot est envoyé comme clé JSON. Gardez-le dans le corps même si l'input est masqué, pour que les bots qui le remplissent se signalent toujours.

## Les deploy previews ont leur propre formulaire

Les deploy previews vivent sur des hôtes comme `deploy-preview-12--your-site.netlify.app`. Ce n'est pas un sous-domaine de `your-site.netlify.app`, il ne correspond donc pas dans **Allowed domains**, et les envois des préproductions finissent en spam. Donnez aux previews et aux branch deploys leur propre formulaire dans `netlify.toml`, et définissez l'en-tête Content-Security-Policy dans le même fichier :

`netlify.toml`:

```toml
[build]
  command = "npm run build"
  publish = "dist"

[context.production.environment]
  VITE_IKIFORM_FORM_ID = "frm_xxxxxxxxxxxxxxxx"

[context.deploy-preview.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[context.branch-deploy.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
```

Laissez vides les **Allowed domains** du formulaire de préproduction, ou ajoutez l'hôte de préproduction exact pendant vos tests. Le formulaire de production reçoit votre domaine et `your-site.netlify.app`. Les en-têtes de `netlify.toml` s'appliquent à tous les contextes de déploiement ; si vous préférez un fichier `_headers`, il doit se retrouver dans le répertoire de publication.

> N'ajoutez jamais `netlify.app` seul dans **Allowed domains**. Tous les sites Netlify correspondraient, y compris des copies de votre formulaire sur les sites d'autres personnes.

## Vérifiez que tout fonctionne

1. Ouvrez une deploy preview, envoyez, et vérifiez que le message arrive dans le formulaire de préproduction.
2. Fusionnez, puis envoyez en production. Vérifiez que le message arrive dans le formulaire de production et que la page **Forms** de Netlify n'affiche rien de nouveau.
3. Vérifiez la boîte de réception de chaque destinataire des notifications. Chacun reçoit un e-mail avec les champs, et **Reply** répond à l'expéditeur quand le formulaire a un champ `email`.
4. Envoyez avec le honeypot rempli, `_gotcha=bot`. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.
5. Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur **Spam**. Le spam reçoit la même réponse de succès qu'une vraie soumission.

## Pièges

| Problème | Que faire |
| --- | --- |
| `404` à l'envoi | L'envoi est parti vers votre propre site. Un build en cache a peut-être encore l'ancienne `action`, ou un appel `fetch` envoie toujours vers `/`. Cherchez `form-name` dans le répertoire de publication. |
| Les soumissions apparaissent toujours dans Netlify | Un formulaire de la page envoie encore vers votre propre site avec `data-netlify` ou `netlify` : Netlify continue donc de le détecter et de stocker ses envois. Retirez l'attribut, réglez l'`action` sur Ikiform et redéployez. |
| `404 not_found` d'Ikiform sur les préproductions uniquement | Le contexte deploy preview n'a pas de `VITE_IKIFORM_FORM_ID`, l'URL se termine donc par `undefined`. Ajoutez-le sous `[context.deploy-preview.environment]`. |
| `403 origin_not_allowed`, ou tous les envois finissent en spam | Le domaine de la page n'est pas dans les **Allowed domains** du formulaire. Ajoutez-le, plus `localhost` pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés. |
| `422 files_not_supported` | Le formulaire a un `<input type="file">` avec un fichier choisi. L'envoi de fichiers n'est pas encore pris en charge ; retirez l'input ou demandez plutôt un lien. |

## Questions

### Puis-je utiliser un autre backend de formulaires tout en restant sur Netlify ?

Oui. Netlify ne gère que les formulaires qu'il a détectés au build. Un formulaire sans data-netlify dont l'action pointe vers un autre endpoint est servi par Netlify comme n'importe quelle page et envoie directement vers cet endpoint.

### Pourquoi mon formulaire reçoit-il 404 sur Netlify ?

Netlify répond 404 à un POST vers une page statique, sauf si Netlify Forms a détecté ce formulaire dans le HTML déployé. Marquez le formulaire pour Netlify Forms, ou faites pointer son action vers un autre backend.

### Puis-je garder Netlify Forms pour un formulaire et utiliser Ikiform pour un autre ?

Oui. La détection se fait par formulaire, via l'attribut. Gardez data-netlify sur les formulaires que Netlify doit gérer et laissez la détection activée.

### Est-ce que je perds mes anciennes soumissions Netlify en changeant ?

Elles restent dans Netlify, pas dans Ikiform. Téléchargez-les depuis la page Forms du site avant de désactiver la détection, et gardez le fichier dans vos archives.

### Le formulaire peut-il accepter des envois de fichiers ?

Pas encore. Un envoi contenant un fichier est refusé avec 422 files_not_supported. Demandez plutôt un lien vers le fichier, comme une URL de drive partagé.

## Autres guides

- [Formulaire de contact sur un site statique](https://ikiform.com/fr/guides/contact-form-static-site.md): Ajoutez un formulaire de contact fonctionnel à un site statique sans serveur : où va le POST, le spam, les réponses par e-mail, et du code pour chaque hébergeur.
- [Vercel](https://ikiform.com/fr/guides/contact-form-static-site/vercel.md): Une page statique sur Vercel répond à un POST de formulaire par 405 Method Not Allowed ; voici pourquoi, et comment envoyer le formulaire à un backend.
- [Cloudflare Pages](https://ikiform.com/fr/guides/contact-form-static-site/cloudflare-pages.md): Un formulaire de contact sur Cloudflare Pages sans écrire de Function : envoyez à un backend, définissez les en-têtes dans _headers, utilisez vos clés Turnstile.
- [GitHub Pages](https://ikiform.com/fr/guides/contact-form-static-site/github-pages.md): GitHub Pages n'exécute pas de code serveur, un POST de formulaire reçoit donc 405 Not Allowed ; envoyez vers un backend et réglez bien les redirections.
- [Amazon S3](https://ikiform.com/fr/guides/contact-form-static-site/aws-s3.md): Les website endpoints S3 n'acceptent que GET et HEAD en HTTP simple : envoyez le formulaire à un backend et gérez bien les redirections avec CloudFront.
- [Site portfolio](https://ikiform.com/fr/guides/contact-form-static-site/portfolio-website.md): Un formulaire de contact de portfolio qui pose les questions d'un premier appel : projet, budget, délai et un lien, sans serveur et avec réponses depuis votre boîte.
