Formulário de contato em site estático7 min de leitura
Adicione um formulário de contato a um site no Cloudflare Pages
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.
Assets estáticos no Cloudflare Pages respondem a um POST com 405, então ou você escreve uma Pages Function ou envia o formulário para um backend de formulários. Com o Ikiform você não escreve Function nenhuma: defina o action do formulário como o seu endpoint, adicione your-project.pages.dev em Allowed domains, o que também cobre todas as URLs de preview, e inclua o Ikiform na Content-Security-Policy do seu _headers. Se você já usa a Cloudflare, pode colocar suas próprias chaves do Turnstile no formulário.
O que muda no Cloudflare Pages
O Pages serve o resultado do seu build pela rede da Cloudflare. Assets estáticos respondem a GET; um POST para um deles recebe 405 sem corpo:
HTTP/2 405
server: cloudflare
content-length: 0O Pages pode rodar código, por meio de Pages Functions numa pasta functions/. Um functions/contact.ts poderia aceitar o envio, mas aí você está escrevendo um backend: parsing, validação, um provedor de e-mail, filtro de spam e armazenamento. A própria documentação da Cloudflare também recomenda começar projetos novos no Workers em vez do Pages. Enviar para um backend de formulários evita as duas coisas, e o mesmo formulário funciona sem mudanças se você migrar depois para os static assets do Workers.
| Característica do Pages | O que significa para o formulário |
|---|---|
URLs de preview são subdomínios, como abc123.your-project.pages.dev | Um único domínio permitido, your-project.pages.dev, cobre a produção e todos os previews. Os previews da Vercel e da Netlify precisam de entradas próprias |
Os headers vêm de um arquivo _headers no resultado do build | Até 100 regras, 2.000 caracteres por linha. Eles não se aplicam a respostas de Pages Functions |
| O Turnstile é um produto da Cloudflare | Você pode usar a verificação gerenciada do Ikiform sem chaves, ou seu próprio widget a partir do painel da Cloudflare |
- O Pages serve a página de contato, com a Content-Security-Policy do seu arquivo _headers.
- O widget do Turnstile roda com a sua site key e coloca um token no formulário.
- O formulário envia os campos e o token ao Ikiform.
- O Ikiform verifica o token com a sua secret key e confere o hostname do token com os seus domínios permitidos.
- Um envio válido é armazenado e enviado por e-mail. Um token ausente, expirado ou reutilizado recebe 403 captcha_failed, e a página reinicia o widget.
O formulário
Esta versão usa o seu próprio widget do Turnstile, o que faz sentido quando você já gerencia o Turnstile na sua conta da Cloudflare. Nas configurações do formulário, defina Spam check como Your own Turnstile keys e cole a site key e a secret. O widget adiciona um campo cf-turnstile-response ao formulário, e o select team diz quem deve responder:
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="team">Who should read this?</label>
<select id="team" name="team">
<option value="support">Support</option>
<option value="sales">Sales</option>
<option value="press">Press</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" minlength="10" required></textarea>
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAxxxxxxxxxxxxxx"></div>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button type="submit">Send</button>
<p id="contact-status" role="status"></p>
</form>
<script src="/contact.js" defer></script>O script mantém os visitantes na página. Um token do Turnstile é de uso único, então depois de qualquer envio que falhe ele reinicia o widget para a próxima tentativa:
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
form.addEventListener("submit", async (event) => {
event.preventDefault();
status.textContent = "Sending";
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) {
form.replaceWith(Object.assign(document.createElement("p"), { textContent: "Sent. We'll reply by email." }));
return;
}
window.turnstile?.reset();
if (result?.error?.code === "captcha_failed") {
status.textContent = "The human check expired. Complete it again, then send.";
return;
}
const fields = result?.error?.fields ?? {};
status.textContent = Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.";
});- Nas configurações do widget do Turnstile na Cloudflare, liste os hostnames em que o formulário roda. O Ikiform também confere o hostname do token com os Allowed domains do formulário.
- Envios sem um token válido são recusados com
403 captcha_failed. Isso inclui um envio de HTML simples feito antes de o widget terminar. - Prefere não ter chave nenhuma? Defina Spam check como Ikiform, apague o widget e o script dele, e o envio HTML roda a verificação em ikiform.com. A versão com
fetchpassa a precisar dechallenge()do SDK; veja a verificação humana.
Suas chaves ou a verificação do Ikiform
| Item | Verificação do Ikiform | Suas próprias chaves do Turnstile |
|---|---|---|
| Chaves para gerenciar | Nenhuma | Uma site key e uma secret do seu painel da Cloudflare |
| Mudança no formulário HTML | Nenhuma. A verificação roda numa página de ikiform.com | Adicionar o widget e o script dele |
Versão com fetch | Chamar challenge() do SDK | Enviar o cf-turnstile-response do widget |
| Content-Security-Policy | Só form-action e connect-src para o Ikiform | Também challenges.cloudflare.com em script-src e frame-src |
| Analytics do Turnstile | Não na sua conta | No seu painel da Cloudflare, junto com os seus outros widgets |
A verificação do Ikiform dá menos trabalho para configurar. Suas próprias chaves fazem sentido quando você já acompanha o analytics do Turnstile, ou quer controlar a aparência e a posição do widget.
Testando o widget localmente
A Cloudflare publica site keys e secrets de teste que sempre passam ou sempre falham, como o par que sempre passa 1x00000000000000000000AA e 1x0000000000000000000000000000000AA. Secrets de produção rejeitam tokens gerados com uma chave de teste, então não misture as duas num formulário. Crie um segundo formulário no Ikiform para o trabalho local, dê a ele o par de chaves de teste e troque o data-sitekey e o id do formulário juntos. Deixe Allowed domains desse formulário vazio: o Ikiform só confere o hostname de um token quando a lista está definida, e a Cloudflare não documenta qual hostname um token de teste informa.
O arquivo _headers
Coloque o _headers no seu diretório de assets estáticos, como public/, para ele acabar no resultado do build. O Turnstile precisa de script-src e frame-src para https://challenges.cloudflare.com, e o Ikiform precisa de form-action e connect-src:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.comSe depois você adicionar uma Pages Function ao mesmo projeto, as respostas dela não recebem esses headers. Defina-os no código da Function.
Se você migrar para o Workers
Migrar o site para os static assets do Workers muda como ele faz deploy, não como o formulário funciona. O formulário continua enviando para ikiform.com, então nada do lado da Cloudflare o trata. Confira duas coisas depois da migração: o novo host workers.dev ou a rota está em Allowed domains, e os seus headers ainda chegam à página, já que o Workers tem o próprio jeito de defini-los.
your-project.pages.dev e o seu domínio personalizado em Allowed domains. Nunca adicione pages.dev sozinho: todo projeto do Pages passaria a corresponder.Confira se funciona
- Faça push de uma branch e abra a URL de preview dela. Envie e confirme que a mensagem chega e não é marcada como spam: o preview é um subdomínio do host do seu projeto.
- Envie de novo em produção, depois volte e envie o mesmo formulário uma segunda vez sem completar o widget. Espere a mensagem de reinício, não um envio armazenado.
- Abra Submissions no painel. O novo envio está lá com todos os campos que você enviou.
- 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. - 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 |
|---|---|
405 com corpo vazio | O formulário enviou para uma URL do Pages. Confira o action publicado, e se nenhuma Function ou regra de redirecionamento reescreve /contact. |
| O widget do Turnstile nunca aparece | A Content-Security-Policy bloqueia challenges.cloudflare.com. Adicione-o a script-src e frame-src, e confira se o _headers está no resultado do build. |
403 captcha_failed em todo envio | A site key e a secret vêm de widgets diferentes, o hostname não está listado no widget, ou uma secret de teste está combinada com uma site key real. |
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. |
Perguntas
Não. Uma Function é uma forma de aceitar o envio, mas aí você mesmo escreve a validação, o envio de e-mail e o filtro de spam. Enviar para um backend de formulários mantém o projeto totalmente estático.
Não. As URLs de preview do Pages são subdomínios de your-project.pages.dev, então permitir esse host cobre todas elas.
Sim. Defina a verificação de spam do formulário como suas próprias chaves do Turnstile e cole a site key e a secret. O Ikiform verifica cada token e confere o hostname dele com os seus domínios permitidos.
Ele enviou para a sua própria URL do Pages, que serve assets estáticos que só respondem a requisições GET. Aponte o action do formulário para um backend de formulários, ou adicione uma Pages Function para esse caminho.
Não, não há plano gratuito. Testar é grátis: você pode enviar 50 envios de teste por mês a partir de localhost antes de pagar. Para entrar no ar, o Pro começa em US$ 6/mês para 500 envios, ou você pode pagar uma vez 10.000 envios por US$ 49 ou 50.000 envios por US$ 149, com créditos que nunca expiram.
Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.