# Como acabar com o spam no formulário de contato de um site estático

> Acabe com o spam num formulário de site estático mesmo sem guardar segredos nem contar envios, com um partial de honeypot, um script seguro e verificações no servidor.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/spam-protection/static-site

Um site estático não tem servidor, então nada na página consegue guardar uma chave secreta, validar um token de captcha ou contar envios por visitante. Essas verificações precisam rodar em outro lugar: numa função no seu host, ou num backend de formulários para o qual seu formulário envia. Coloque um honeypot acessível no formulário de qualquer jeito e mantenha o formulário funcionando sem JavaScript.

## O que muda num site estático

Num site estático, cada byte que você publica é público. Tudo no seu HTML ou JavaScript pode ser lido e pulado por um bot, então verificações de spam no cliente são conselho, não imposição. As três verificações que mais importam precisam de um servidor:

- **Validar um token de captcha** exige uma chave secreta, que não pode ficar num arquivo público.
- **Contar envios por visitante** exige um armazenamento que sobreviva entre requisições.
- **Ler um honeypot** exige código que rode depois que o envio chega, fora do alcance do bot.

Hosts estáticos também não aceitam envios para as suas páginas. A Vercel e o GitHub Pages respondem a um `POST` de formulário para uma página estática com `405 Method Not Allowed`, então o formulário precisa de um endpoint antes mesmo de o spam virar questão.

Diagrama: Três jeitos de ligar um formulário de site estático, e onde as verificações de spam rodam em cada um

1. Uma página estática que envia para si mesma chega a um host que não roda código, então o envio falha com 405 ou não vai a lugar nenhum.
2. Com uma função no host, a página envia a uma função que guarda o segredo, lê o honeypot e valida o token, e depois encaminha a mensagem.
3. Com um backend de formulários, a página envia direto ao backend, que roda o honeypot, a pontuação, o limite de taxa e a verificação humana antes de mandar o e-mail para você.

Nas duas linhas que funcionam, as verificações rodam depois que o envio sai do navegador. É o único lugar onde um bot não consegue pulá-las.

## Suas opções

| Opção | Verificações de spam | Prós e contras |
| --- | --- | --- |
| Netlify Forms | Akismet em todo envio, mais um campo `netlify-honeypot` e o reCAPTCHA v2 se você os adicionar | Só na Netlify. Mudar de host significa refazer o formulário. Veja [alternativas ao Netlify Forms](https://ikiform.com/alternatives/netlify-forms) |
| Uma função no seu host | O que você escrever: honeypot, validação de token, limite de taxa com armazenamento | Você escreve e mantém, e precisa de um serviço de e-mail ou de armazenamento por trás |
| Um backend de formulários, como o Ikiform | Honeypot, pontuação, limites de taxa, domínios permitidos e verificação humana, sem código | Mais um serviço, cobrado por envio |

## O formulário, como um partial reutilizável

Seja qual for o endpoint escolhido, escreva o formulário uma vez como partial ou include, para que toda página use o mesmo honeypot. A versão abaixo envia ao Ikiform. Para usar uma função, mude o `action` para o caminho da função e tire o `_redirect`.

**Hugo** (`layouts/partials/contact-form.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ "thanks/" | absURL }}" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

**Jekyll** (`_includes/contact-form.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ '/thanks/' | absolute_url }}" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

**Eleventy** (`_includes/contact-form.njk`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ site.url }}/thanks/" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

`assets/contact.css`:

```html
<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>
```

- O honeypot fica fora da tela, fora da ordem de tabulação e oculto para leitores de tela. O [guia do honeypot](https://ikiform.com/pt-br/guides/spam-protection/honeypot.md) explica cada atributo.
- `_redirect` leva as pessoas para a sua página de agradecimento. O Ikiform só o segue para um endereço `https:` num dos **Allowed domains** do formulário.
- Os guias de [Hugo](https://ikiform.com/pt-br/docs/guides/hugo.md), [Jekyll](https://ikiform.com/pt-br/docs/guides/jekyll.md) e [Eleventy](https://ikiform.com/pt-br/docs/guides/eleventy.md) cobrem configuração, shortcodes e GitHub Pages.

## Melhore sem quebrar

Um script pode manter os visitantes na página, mas nunca pode ser o único jeito de o formulário funcionar. Este envia com `fetch` e volta para um envio normal do navegador em dois casos: quando a chamada de rede falha e quando a verificação humana do Ikiform pede um token com `challenge_required`. Um envio normal recebe a página de verificação do Ikiform, então você pode ligar a verificação sem mudar o script.

`contact-form.html`:

```html
<script>
  document.querySelectorAll("form.contact").forEach((form) => {
    const status = form.querySelector("[role=status]");
    const button = form.querySelector("button");

    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      button.disabled = true;
      try {
        const response = await fetch(form.action, {
          method: "POST",
          headers: { Accept: "application/json" },
          body: new FormData(form),
        });
        const result = await response.json();
        if (result.ok) {
          form.replaceChildren("Thanks. We'll reply by email.");
          return;
        }
        if (result.error.code === "challenge_required") {
          form.submit();
          return;
        }
        const wait = response.headers.get("Retry-After");
        if (response.status === 429 && wait) {
          status.textContent = `Too many messages. Try again in ${wait} seconds.`;
        } else {
          status.textContent = result.error.message;
        }
      } catch {
        form.submit();
        return;
      }
      button.disabled = false;
    });
  });
</script>
```

- `form.submit()` envia o formulário sem disparar o evento `submit` de novo, então o fallback não entra em loop.
- Um `429` mostra quanto esperar a partir do `Retry-After`, que o Ikiform expõe ao código do navegador.
- A linha de sucesso substitui o formulário, para ninguém enviá-lo duas vezes sem querer.

## Se você escrever uma função no host

Netlify, Cloudflare Pages e Vercel rodam pequenas funções ao lado dos arquivos estáticos. Uma função é o seu próprio backend, então as verificações de spam ficam por conta dela. Esta Netlify Function lê o honeypot, descarta mensagens entupidas de links, valida um token do Turnstile e manda o e-mail para você pelo Resend. Todo envio barrado recebe o mesmo redirecionamento de agradecimento que um real.

`netlify/functions/contact.mts`:

```ts
import type { Config, Context } from "@netlify/functions";

const LINK = /https?:\/\/|www\./gi;

export default async (req: Request, context: Context) => {
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
  const form = await req.formData();
  const email = String(form.get("email") ?? "");
  const message = String(form.get("message") ?? "");
  const thanks = Response.redirect(new URL("/thanks/", req.url), 303);

  if (String(form.get("_gotcha") ?? "").trim()) return thanks;
  if ((message.match(LINK)?.length ?? 0) >= 3) return thanks;

  const check = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
    method: "POST",
    body: new URLSearchParams({
      secret: process.env.TURNSTILE_SECRET_KEY ?? "",
      response: String(form.get("cf-turnstile-response") ?? ""),
      remoteip: context.ip,
    }),
  });
  const verdict = (await check.json()) as { success: boolean };
  if (!verdict.success) return Response.redirect(new URL("/contact/?error=check", req.url), 303);

  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      from: "Contact form <form@example.com>",
      to: "you@example.com",
      reply_to: email,
      subject: "New message from the website",
      text: message,
    }),
  });
  return thanks;
};

export const config: Config = { path: "/api/contact" };
```

- `context.ip` é o IP do visitante, que o siteverify pode usar como `remoteip`.
- Defina `TURNSTILE_SECRET_KEY` e `RESEND_API_KEY` nas variáveis de ambiente do site, nunca no repositório.
- Ela descarta os envios barrados em vez de guardá-los, então uma mensagem real barrada por engano se perde. No mínimo, registre-os em log.
- Ela não tem limite de taxa, já que uma função não tem memória entre chamadas. O [guia de limite de taxa](https://ikiform.com/pt-br/guides/spam-protection/rate-limiting.md) adiciona um com Redis.

## O que o Ikiform faz por um formulário estático

Com o formulário enviando direto ao Ikiform, o trabalho do lado do servidor acontece lá. O honeypot é `_gotcha`, a pontuação de conteúdo e os limites de taxa por visitante rodam em todo envio, e **Allowed domains** marca como spam os envios de outros sites. Mude **Spam check** para **Ikiform** para ter o Cloudflare Turnstile sem chaves: o envio HTML simples o roda numa página de verificação, e o script acima volta para essa página. Um envio julgado como spam recebe a mesma resposta de sucesso que um real, então o bot não aprende nada. Ele é guardado com o status `spam`, não dispara e-mail de notificação nem webhook, não conta no seu uso e é apagado após 30 dias. Toda segunda-feira, um workspace que bloqueou spam recebe um único e-mail de resumo com o total, os formulários mais visados e os principais motivos.

> Liste em **Allowed domains** todo host onde o formulário é publicado: seu domínio, mais o endereço do próprio host enquanto você testa, como `username.github.io`. Deploy previews da Netlify e URLs de preview da Vercel não são subdomínios do endereço principal do seu site; adicione o host exato do preview enquanto testa um.

## Confira se funciona

1. Desative o JavaScript e envie. O formulário ainda envia e você cai na sua página de agradecimento.
2. Ative o JavaScript e envie. O formulário é substituído pela linha de agradecimento.
3. Envie com o honeypot preenchido, `_gotcha=bot`. Ele ainda recebe uma resposta de sucesso, mas o envio é guardado como spam e não dispara e-mail nem webhook.
4. Defina **Spam check** como **Ikiform** e envie com o script ligado. A página volta para um envio normal e mostra a página de verificação.
5. Nada chegou? Mude o filtro de status do painel para **Spam**. O spam recebe a mesma resposta de sucesso que um envio real.

## Pegadinhas

- **`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, e também `localhost` para testes locais. Subdomínios de um domínio listado são permitidos.
- **HTML minificado.** Algumas etapas de build removem atributos que julgam vazios ou sem uso. Confira se a página gerada ainda tem `aria-hidden` e `tabindex` no honeypot.
- **Páginas de agradecimento em cache.** Se a página de agradecimento for a mesma URL para spam e envios reais, como deve ser, nunca coloque nela nada que dependa de o envio ser real.
- **Chaves no repositório.** Um segredo commitado no repositório de um site estático público é público. Troque-o se isso já aconteceu.

## Perguntas

### Posso adicionar um captcha a um site estático?

Você pode mostrar o widget, mas o token dele precisa ser validado com uma chave secreta num servidor. Use uma função no seu host, um backend de formulários que valide suas chaves ou a verificação gerenciada do Ikiform, que dispensa chaves.

### Um honeypot basta para um site estático?

Ele barra os bots mais comuns, mas só se algo num servidor o ler. Combine-o com um limite de taxa e pontuação de conteúdo, que também precisam de um servidor ou de um backend de formulários.

### Por que meu formulário estático mostra 405 Method Not Allowed?

O formulário envia para uma página num host que só serve arquivos. Aponte o action do formulário para uma função ou para o endpoint de um backend de formulários.

### O formulário funciona com o JavaScript desativado?

Deveria. Envie primeiro com um formulário HTML simples e adicione um script que o melhore e volte para um envio normal quando algo der errado.

## Mais guias

- [Acabe com o spam no formulário de contato](https://ikiform.com/pt-br/guides/spam-protection.md): Acabe com o spam no formulário de contato com camadas que não custam nada a pessoas reais: honeypot, tempo, pontuação de conteúdo, limites de taxa e verificação humana só quando precisar.
- [Cloudflare Turnstile](https://ikiform.com/pt-br/guides/spam-protection/cloudflare-turnstile.md): Adicione o Cloudflare Turnstile a um formulário HTML simples: o widget, o siteverify no servidor, chaves de teste e um jeito de rodá-lo sem chaves nem servidor próprio.
- [Campo honeypot](https://ikiform.com/pt-br/guides/spam-protection/honeypot.md): Adicione a qualquer formulário um honeypot que pega bots sem atrapalhar leitores de tela, teclados ou preenchimento automático, com a verificação no servidor para cada stack.
- [Limite de taxa](https://ikiform.com/pt-br/guides/spam-protection/rate-limiting.md): Limite a taxa de envios por visitante com um armazenamento compartilhado, responda 429 com Retry-After e evite bloquear escritórios que compartilham um IP.
- [Sem captcha](https://ikiform.com/pt-br/guides/spam-protection/without-captcha.md): Barre a maior parte do spam em formulários sem captcha nenhum: honeypot, horário de renderização assinado, pontuação de conteúdo e limite de taxa, somados em vez de usados sozinhos.
- [Alternativas ao reCAPTCHA](https://ikiform.com/pt-br/guides/spam-protection/recaptcha-alternatives.md): Compare alternativas ao reCAPTCHA para formulários, do Turnstile e hCaptcha a prova de trabalho e nenhum captcha, e migre com uma mudança pequena e segura.
