# Adicione um formulário de contato na Netlify sem o Netlify Forms

> Rode um formulário de contato num site da Netlify sem o Netlify Forms: sem detecção no build, envios JSON a partir do React e um formulário separado para deploy previews.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/contact-form-static-site/netlify

Deixe o `data-netlify` fora do formulário e aponte o `action` dele para o seu endpoint do Ikiform; assim a Netlify serve a página e nunca mexe no envio. Formulários em React e Vue não precisam de uma cópia HTML oculta e podem enviar JSON. Defina um id de formulário diferente para os deploy previews no `netlify.toml`, porque os hosts de preview não correspondem ao seu site em **Allowed domains**.

## O que muda na Netlify

A Netlify é o único grande host estático com um recurso de formulários embutido. O Netlify Forms lê o HTML publicado depois de cada build, encontra os formulários marcados com `data-netlify` ou `netlify` e aceita envios para o seu próprio site nesses formulários. Todo outro envio para uma página estática falha, e na Netlify ele falha com `404`, não `405`:

`Terminal`:

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

`Resposta`:

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

Esse `404` faz as pessoas caçarem uma página faltando que não está faltando. Ele significa que nenhum formulário detectado correspondeu ao envio. Usar ou não o Netlify Forms é outra questão, e a [comparação entre Ikiform e Netlify Forms](https://ikiform.com/alternatives/netlify-forms) responde com honestidade: se o seu site vai ficar para sempre na Netlify e o formulário é HTML simples, o Netlify Forms dá menos trabalho. Esta página é para os outros casos.

| Sua situação | Por que o Netlify Forms atrapalha |
| --- | --- |
| O formulário é renderizado por React, Vue ou Svelte | O build analisa HTML estático, então um formulário renderizado no cliente precisa de uma cópia oculta para a Netlify encontrar |
| Você quer enviar JSON com `fetch` | O Netlify Forms só aceita corpos urlencoded e multipart |
| O site pode mudar de host | O formulário envia para o seu próprio domínio, então só funciona enquanto a Netlify servir esse domínio |
| As mensagens devem chegar a um webhook que você verifica | O Ikiform assina cada entrega com headers do Standard Webhooks e tenta de novo quando falha |

Diagrama: Netlify Forms e Ikiform lado a lado no mesmo site da Netlify

1. Com o Netlify Forms, a Netlify analisa o HTML de cada deploy em busca de formulários marcados com data-netlify.
2. Um formulário encontrado aceita envios para o seu próprio site, e a Netlify os armazena.
3. Um formulário não encontrado, como um renderizado por JavaScript sem cópia oculta, recebe 404 em todo envio.
4. Com o Ikiform, nada precisa ser encontrado no build.
5. O formulário envia direto para ikiform.com, como HTML ou JSON, e o Ikiform armazena e manda um e-mail para você.

Os dois podem conviver num site. Um formulário com data-netlify vai para a Netlify; um formulário cujo action é um endpoint do Ikiform vai para o Ikiform.

## Tire um formulário do Netlify Forms

- Remova `data-netlify` ou `netlify`, `netlify-honeypot` e `data-netlify-recaptcha` do formulário, e qualquer input oculto `form-name`. Se ficar, o `form-name` é armazenado como um campo comum.
- Apague a cópia estática oculta de um formulário JavaScript, como a de baixo. A Netlify só precisava dela para detectar o formulário.
- Defina o `action`, ou a URL do `fetch`, como o seu endpoint do Ikiform. O antigo `action="/thanks"` vai para a configuração **Redirect after submit** do formulário.
- Baixe antes os envios antigos na página **Forms** da Netlify. Eles não migram sozinhos.

`index.html, apague isto`:

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

Deixe o **form detection** ligado se outros formulários do site ainda usam o Netlify Forms. A documentação da Netlify diz que desligar a detecção é só para sites que não usam o Netlify Forms. Quando nada mais o usar, desligá-lo economiza uma análise a cada deploy.

### O que substitui as configurações de spam da Netlify

A Netlify roda o Akismet nos envios e oferece `netlify-honeypot` e reCAPTCHA. No Ikiform, o honeypot continua funcionando se você definir o **Honeypot field** do formulário com o nome que já usa, como `bot-field`. A pontuação de spam roda em todo envio sem configuração, e o **Spam check** definido como **Ikiform** troca o reCAPTCHA pelo Cloudflare Turnstile, sem chaves. Remova a `div` do reCAPTCHA quando fizer a troca.

## Um formulário React que envia JSON

Sem o Netlify Forms no caminho, um formulário renderizado no cliente não precisa de cópia e pode enviar JSON, o que mantém os tipos intactos e permite limpar os valores antes. Este também registra qual contexto da Netlify gerou a página:

`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` diz ao Ikiform para interpretar JSON; `Accept: application/json` pede uma resposta em JSON.
- `VITE_CONTEXT` é um nome de exemplo. A Netlify define `CONTEXT` durante o build; exponha-a ao Vite no seu comando de build, ou remova o campo.
- O honeypot é enviado como uma chave JSON. Mantenha-o no corpo mesmo com o input oculto, para que os bots que o preenchem continuem se denunciando.

## Os deploy previews ganham um formulário próprio

Os deploy previews ficam em hosts como `deploy-preview-12--your-site.netlify.app`. Isso não é um subdomínio de `your-site.netlify.app`, então não corresponde em **Allowed domains**, e os envios de preview caem no spam. Dê aos previews e aos branch deploys um formulário próprio no `netlify.toml`, e defina o header Content-Security-Policy no mesmo arquivo:

`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"
```

Deixe **Allowed domains** do formulário de preview vazio, ou adicione o host de preview exato enquanto testa. O formulário de produção fica com o seu domínio e `your-site.netlify.app`. Os headers no `netlify.toml` valem para todos os contextos de deploy; se você preferir um arquivo `_headers`, ele precisa acabar no diretório de publicação.

> Nunca adicione `netlify.app` sozinho em **Allowed domains**. Todo site da Netlify passaria a corresponder, inclusive cópias do seu formulário em sites de outras pessoas.

## Confira se funciona

1. Abra um deploy preview, envie e confirme que a mensagem cai no formulário de preview.
2. Faça o merge e envie em produção. Confirme que a mensagem cai no formulário de produção e que a página **Forms** da Netlify não mostra nada novo.
3. Confira a caixa de entrada de cada destinatário das notificações. Cada um recebe um e-mail com os campos, e **Reply** vai para quem enviou quando o formulário tem um campo `email`.
4. Envie com o honeypot preenchido, `_gotcha=bot`. Ele ainda recebe uma resposta de sucesso, mas o envio é armazenado como spam e não dispara e-mail nem webhook.
5. Não chegou nada? Mude o filtro de status do painel para **Spam**. Spam recebe a mesma resposta de sucesso de um envio real.

## Pegadinhas

| Problema | O que fazer |
| --- | --- |
| `404` ao enviar | O envio foi para o seu próprio site. Um build em cache pode ainda ter o `action` antigo, ou uma chamada `fetch` ainda envia para `/`. Procure `form-name` no diretório de publicação. |
| Os envios ainda aparecem na Netlify | Algum formulário da página ainda envia para o seu próprio site com `data-netlify` ou `netlify`, então a Netlify continua detectando e armazenando. Remova o atributo, defina o `action` como o Ikiform e faça o deploy de novo. |
| `404 not_found` do Ikiform só nos previews | O contexto de deploy preview não tem `VITE_IKIFORM_FORM_ID`, então a URL termina em `undefined`. Adicione-a em `[context.deploy-preview.environment]`. |
| `403 origin_not_allowed`, ou todo envio cai no spam | O domínio da página não está em **Allowed domains** do formulário. Adicione-o, mais `localhost` para testes locais. Subdomínios de um domínio listado são permitidos. |
| `422 files_not_supported` | O formulário tem um `<input type="file">` com um arquivo escolhido. Upload de arquivos ainda não é suportado; remova o input ou peça um link. |

## Perguntas

### Posso usar outro backend de formulários enquanto meu site continua na Netlify?

Sim. A Netlify só trata os formulários que detectou no build. Um formulário sem data-netlify cujo action aponta para outro endpoint é servido pela Netlify como qualquer página e envia direto para esse endpoint.

### Por que meu formulário recebe 404 na Netlify?

A Netlify responde a um POST para uma página estática com 404, a menos que o Netlify Forms tenha detectado aquele formulário no HTML publicado. Marque o formulário para o Netlify Forms ou aponte o action dele para outro backend.

### Posso manter o Netlify Forms num formulário e usar o Ikiform em outro?

Sim. A detecção é por formulário, pelo atributo. Mantenha data-netlify nos formulários que a Netlify deve tratar e deixe o form detection ligado.

### Perco meus envios antigos da Netlify quando eu trocar?

Eles ficam na Netlify, não no Ikiform. Baixe-os na página Forms do site antes de desligar o form detection, e guarde o arquivo com os seus registros.

### O formulário aceita upload de arquivos?

Ainda não. Um envio que contém um arquivo é recusado com 422 files_not_supported. Peça um link para o arquivo, como a URL de um drive compartilhado.

## Mais guias

- [Formulário de contato em site estático](https://ikiform.com/pt-br/guides/contact-form-static-site.md): Coloque um formulário de contato funcionando num site estático sem servidor: para onde vai o POST, spam, respostas por e-mail e código para todo host e framework.
- [Vercel](https://ikiform.com/pt-br/guides/contact-form-static-site/vercel.md): Uma página estática na Vercel responde a um POST de formulário com 405 Method Not Allowed; veja por quê e como enviar o formulário para um backend.
- [Cloudflare Pages](https://ikiform.com/pt-br/guides/contact-form-static-site/cloudflare-pages.md): Adicione um formulário de contato ao Cloudflare Pages sem escrever uma Function: envie para um backend de formulários, defina headers no _headers e use suas próprias chaves do Turnstile.
- [GitHub Pages](https://ikiform.com/pt-br/guides/contact-form-static-site/github-pages.md): O GitHub Pages não roda código de servidor, então um POST de formulário recebe 405 Not Allowed; envie para um backend de formulários e acerte os redirecionamentos de project sites.
- [Amazon S3](https://ikiform.com/pt-br/guides/contact-form-static-site/aws-s3.md): Endpoints de site do S3 só aceitam GET e HEAD via HTTP simples, então envie o formulário de contato para um backend de formulários e acerte os redirecionamentos com o CloudFront.
- [Site de portfólio](https://ikiform.com/pt-br/guides/contact-form-static-site/portfolio-website.md): Um formulário de contato para portfólio que faz as perguntas de uma primeira conversa: projeto, orçamento, prazo e um link, sem servidor e com respostas pela sua caixa de entrada.
