Formulário de contato em site estático

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.

Preet SutharFundador, Ikiform

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:

Resposta a um POST
HTTP/2 405
server: cloudflare
content-length: 0

O 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 PagesO que significa para o formulário
URLs de preview são subdomínios, como abc123.your-project.pages.devUm ú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 buildAté 100 regras, 2.000 caracteres por linha. Eles não se aplicam a respostas de Pages Functions
O Turnstile é um produto da CloudflareVocê pode usar a verificação gerenciada do Ikiform sem chaves, ou seu próprio widget a partir do painel da Cloudflare
Um formulário de contato no Cloudflare Pages com suas próprias chaves do Turnstilepáginanopages.devservidacom_headerswidgetdoTurnstilesua sitekeyIkiformverificacom a suasecretarmazenadoeenviadopore-mail403captcha_failedreiniciaro widgettokenPOST + tokentoken ruim ou usadoUm formulário de contato no Cloudflare Pages com suas próprias chaves do Turnstilepágina nopages.devservida com _headerswidget doTurnstilesua site keyIkiformverifica com a suasecretarmazenado eenviado pore-mail403captcha_failedreiniciar owidgettokenPOST + tokentoken ruim ou usado
  1. O Pages serve a página de contato, com a Content-Security-Policy do seu arquivo _headers.
  2. O widget do Turnstile roda com a sua site key e coloca um token no formulário.
  3. O formulário envia os campos e o token ao Ikiform.
  4. O Ikiform verifica o token com a sua secret key e confere o hostname do token com os seus domínios permitidos.
  5. 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.
Com a verificação gerenciada do Ikiform, não há widget na sua página; o formulário HTML não precisa de mudança nenhuma.

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:

public/contact.html
<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:

public/contact.js
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 fetch passa a precisar de challenge() do SDK; veja a verificação humana.

Suas chaves ou a verificação do Ikiform

ItemVerificação do IkiformSuas próprias chaves do Turnstile
Chaves para gerenciarNenhumaUma site key e uma secret do seu painel da Cloudflare
Mudança no formulário HTMLNenhuma. A verificação roda numa página de ikiform.comAdicionar o widget e o script dele
Versão com fetchChamar challenge() do SDKEnviar o cf-turnstile-response do widget
Content-Security-PolicySó form-action e connect-src para o IkiformTambém challenges.cloudflare.com em script-src e frame-src
Analytics do TurnstileNão na sua contaNo 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:

public/_headers
/*
  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.com

Se 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.

Adicione 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

  1. 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.
  2. 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.
  3. Abra Submissions no painel. O novo envio está lá com todos os campos que você enviou.
  4. 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.
  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

ProblemaO que fazer
405 com corpo vazioO 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 apareceA 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 envioA 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 spamO 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

Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.