formulários

Formulários que funcionam sem backend

Adicione um atributo a um formulário do seu site estático e cada envio cai no seu painel. Sem JavaScript, sem widget incorporado, sem serviço de terceiros e sem nada para quem preenche ter que assinar.

Its markup, styles and deploy script are on GitHub — copy it, change the fields, redeploy.

index.html
<form harvis-form="contact">
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <button>Send</button>
</form>
how it works

Três passos, e um deles é publicar de novo

Não há construtor de formulários para aprender nem endpoint para copiar. O atributo é toda a configuração.

  1. 01

    Adicione o atributo

    Coloque harvis-form em qualquer formulário do seu HTML. Dê um nome a ele se o site tiver mais de um. Deixe action e method de fora — eles são preenchidos na hora em que a página é servida.

  2. 02

    Publique de novo

    Arraste a pasta outra vez ou rode npx harvis. Nada para configurar, nenhuma chave para colar, nenhum botão para ligar antes no painel.

  3. 03

    Leia o que chegar

    Os envios aparecem embaixo do seu site no painel, os mais recentes primeiro. Abra uma linha para ler tudo, ou leve o conjunto inteiro em CSV.

the markup

A coisa toda são dois atributos

O harvis reescreve o formulário na saída do servidor, então a página que você escreveu continua sendo a página que você escreveu.

o que você escreve
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html">
  <input name="email" type="email" required>
  <button>Send</button>
</form>
o que seus visitantes recebem
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html"
      action="/__harvis/form/contact" method="post">
  <input type="hidden" name="_harvis_redirect" value="/thanks.html">
  <input type="text" name="_harvis_hp" tabindex="-1" aria-hidden="true" style="…">
  <input name="email" type="email" required>
  <button>Send</button>
</form>

Os dois atributos

harvis-form
Marca o formulário como um dos que devem ser coletados. O valor dá nome a ele, para que vários formulários de um mesmo site não se misturem; sem valor, a coleta vai para “default”.
data-harvis-redirect
Opcional. A página do seu site para onde mandar as pessoas depois do envio. Qualquer coisa que aponte para fora do seu site é ignorada.
frameworks

Quando é o JavaScript que monta o formulário, escreva o endpoint você mesmo

O harvis preenche o action na hora em que a página sai do servidor. Um formulário que só existe depois que seu bundle roda ainda não está na página, então não há o que preencher — você escreve o que a reescrita teria escrito. Continua sendo um post HTML comum, e todo o resto desta página continua valendo.

  • React
  • Vue
  • Svelte
  • Angular
ContactForm.jsx
<form action="/__harvis/form/contact" method="post">
  <input name="email" type="email" required />
  <textarea name="message" />

  {/* optional — where to land after sending */}
  <input type="hidden" name="_harvis_redirect" value="/thanks" />

  {/* optional — the decoy harvis would have added */}
  <input
    type="text"
    name="_harvis_hp"
    tabIndex={-1}
    autoComplete="off"
    aria-hidden="true"
    style={{ position: "absolute", left: "-9999px", opacity: 0 }}
  />

  <button>Send</button>
</form>
action + method
O endpoint que o harvis teria escrito, mais post. O último trecho é o nome do formulário — letras minúsculas, números e hifens; qualquer outra coisa é recolhida em “default”.
_harvis_redirect
Opcional, e a versão escrita à mão do data-harvis-redirect. Um caminho no seu próprio site; qualquer coisa que aponte para fora é ignorada.
_harvis_hp
Opcional, e a única coisa que você perde ao escrever o formulário sozinho: a isca que pega robôs. Deixe-a fora da tela em vez de display:none, e vazia — tudo o que a preencher é descartado.
harvis-form
Deixe de fora. O atributo é um pedido para o harvis escrever o action, e você acabou de escrever você mesmo.

Deixe o navegador enviar

Sem preventDefault, sem fetch. A resposta é um redirecionamento que o navegador segue sozinho, e é isso que mantém o formulário funcionando quando os scripts não rodam.

Pré-renderizado conta como JavaScript

Se o seu build escreve o formulário no HTML e o seu app assume ele no navegador depois, escreva os campos mesmo assim — os que o harvis inseriu não estão no seu componente, então a hidratação pode jogá-los fora.

ai agents

Hand it to the agent that wrote your site

If an AI built the site, it can wire the forms up too. Copy the brief below into whatever has your project open — Claude Code, Cursor, Copilot, the chat you built the page in — and it will find the forms you already have and convert them. It covers both cases on this page, so you don't have to know which one you're in.

paste this into your ai agent
Wire the forms in this project up to harvis.dev (https://harvis.dev), which collects form submissions for static sites that have no backend. Read all of this before editing anything.

HOW IT WORKS
harvis serves every page of a site it hosts, so it can take a form post on the way past. Marking a <form> with the harvis-form attribute is the whole integration: as the page is served, harvis rewrites that tag to action="/__harvis/form/<name>" method="post", inserts a hidden honeypot field, and turns data-harvis-redirect into a hidden field. The form then posts natively, same-origin, with no JavaScript, no API key and no third-party service. Submissions appear in the site owner's dashboard (https://harvis.dev/dashboard) and are emailed to them.

STEP 1 — FIND THE FORMS
Look for every <form> in the project: .html files, and templates or components if a framework is in use. Skip search boxes and any form that posts to an API this project owns and expects a reply from. For each remaining form, decide which case it is:
- CASE A — the form is in the HTML that gets deployed: plain HTML, or a generator/framework that renders it at build time. This is the common case.
- CASE B — the form only exists once JavaScript has run: a React/Vue/Svelte/Angular component mounted in the browser, or markup a script writes. harvis rewrites the served HTML, so a form that is not in it yet is never rewritten. A form that is prerendered and then hydrated is CASE B too, because the fields harvis inserts are not in the component tree and hydration can discard them.

STEP 2A — CASE A: ADD THE ATTRIBUTE
- Add harvis-form="<name>" to the opening <form> tag. Name it for what it is — contact, signup, feedback. The name must match ^[a-z0-9][a-z0-9_-]{0,39}$ or it is collected under "default". Two forms on one site should not share a name unless they should share one list of submissions.
- Delete that form's existing action and method attributes. harvis overwrites both, so leaving them there only misleads whoever reads the file next.
- Optional: add data-harvis-redirect="/thanks.html" to choose where the visitor lands after sending. It must be a path on this same site and the file must exist in the deploy; anything pointing off-site is ignored. Without it, visitors get a plain harvis confirmation page.
- Do not add a honeypot, _harvis_hp or _harvis_redirect by hand — harvis inserts them, and a second copy is a bug.

STEP 2B — CASE B: WRITE THE ENDPOINT YOURSELF
- Set action="/__harvis/form/<name>" and method="post" on the form, and do NOT add harvis-form: that attribute is a request to rewrite, and here you have written the rewrite yourself.
- Optional redirect: a hidden input named _harvis_redirect whose value is a path on this site.
- Optional honeypot, since nothing will add one for you: an empty text input named _harvis_hp with tabindex -1, autocomplete off, aria-hidden true, positioned off-screen with position:absolute;left:-9999px rather than display:none.
- Let the browser submit it: no onSubmit handler, no preventDefault, no fetch. The reply is a 303 that the browser follows on its own, and intercepting it is what breaks the form when scripts fail.

STEP 3 — IN BOTH CASES
- Every field to be collected needs a name attribute; an input without one is never submitted. Those names become the column headings the owner reads, so prefer name, email and message over field1.
- Remove what is left of any other form service: a Formspree, Getform, Basin or FormSubmit action URL, Netlify's data-netlify attribute and its hidden form-name input, a Web3Forms access_key input, and any handler that POSTed the form somewhere else.
- Keep the client-side validation as it is. required, type="email", minlength and the rest all still work.
- File inputs are dropped: submissions are stored as text and files are not kept. If a form has one, say so rather than leaving it in silently.
- Add no script, SDK, key or config file. There is nothing to install.
- Limits, worth mentioning if a form is likely to meet one: 30 fields per submission, 5,000 characters per field, 64 KB per submission, 60 submissions per site an hour, 20 per visitor an hour, and the newest 1,000 per site are kept.

STEP 4 — DEPLOY AND REPORT
- The attribute only does anything on a served page, so deploy the site again: run npx harvis from the site folder, or tell the user to drag the folder onto https://harvis.dev/drop.
- Then tell the user which files changed, the name you gave each form, and that submissions arrive at https://harvis.dev/dashboard and by email — noting that a site deployed anonymously has no owner to email until it is claimed with the claim link.
- Suggest they send one test submission through the live site.

What it tells the agent to do

  • Find every form in the project and leave the search boxes and API calls alone.
  • Add harvis-form with a sensible name, and strip the action and method that harvis replaces anyway.
  • Write the endpoint by hand instead when the form only exists after JavaScript runs.
  • Clear out whatever the last form service left behind, and flag a file upload as something that won't be kept.
  • Redeploy, then tell you what changed and where the submissions land.

Or leave it in the repo

The same text works as a file — save it as AGENTS.md, CLAUDE.md or a project rule and an agent reads it on its own, so the next form someone adds is collected without anyone asking.

included

O que você ganha

Tudo aqui embaixo já vem ligado. Não existe página de configurações para nada disso.

  • Sem JavaScript

    O formulário envia do jeito que HTML sempre enviou. Continua funcionando com scripts bloqueados, num celular lento e num navegador que só desenha texto.

  • O spam é filtrado

    Todo formulário é servido com um campo-isca que nenhuma pessoa consegue ver, e o que preencher ele é descartado sem aviso. Limites de taxa controlam quanto um visitante, e um site, podem enviar por hora.

  • Quantos formulários você quiser

    Dê nomes a eles — um formulário de contato e um de cadastro no mesmo site mantêm cada um sua própria lista, e você pode filtrar e exportar separadamente.

  • Sua própria página de obrigado

    Aponte o formulário para uma página que você escreveu e é ali que as pessoas caem depois de enviar. Deixe de fora e elas veem uma página de confirmação simples.

  • Exporte quando quiser

    Um botão te dá um CSV com uma coluna para cada campo que alguém já enviou, então um formulário que ganhou uma pergunta no meio do caminho ainda abre como uma tabela só.

  • Nada fica seguindo ninguém

    Sem script de rastreamento, sem cookie, sem terceiros dentro da página. O endereço do visitante passa por hash antes de ser guardado, e só para que o limite de taxa consiga distinguir dois envios.

limits

Os limites

Generosos para um formulário de contato, apertados o bastante para que um script apontado para o seu site não encha sua caixa de entrada.

Campos por envio
30
Tamanho de um campo
5.000 caracteres
Tamanho de um envio
64 KB
Envios por site
60 por hora
Envios por visitante
20 por hora
Guardados por site
1.000, os mais recentes primeiro
faq

Perguntas

O que as pessoas perguntam antes de colocar um formulário num site estático.

O que é um formulário estático?

Um formulário num site que não tem servidor próprio. Normalmente isso quer dizer que o formulário não tem para onde mandar nada, e é por isso que sites estáticos costumam pegar emprestado um serviço de formulários de terceiros. O harvis já serve todas as páginas do seu site, então dá para receber o envio na passagem.

Preciso saber programar?

Você precisa conseguir adicionar uma palavra a uma linha de HTML. Se foi uma IA que escreveu seu site, peça que ela adicione o atributo harvis-form ao formulário — ela sabe como, porque é isso que dizem as instruções que o harvis publica para assistentes de IA.

Funciona com o JavaScript desligado?

Funciona. É justamente por isso que foi feito assim. O formulário faz um post HTML comum e o navegador segue um redirecionamento depois, exatamente como os formulários funcionavam antes de o JavaScript existir.

Posso continuar usando meu próprio endpoint ou outro serviço de formulários?

Pode — é só não adicionar o atributo. Formulários sem harvis-form são servidos exatamente como você escreveu, action e tudo. Num formulário que tem o atributo, o harvis substitui o action, porque adicionar o atributo é justamente o jeito de dizer para onde o envio deve ir.

Funciona com React, Vue ou Svelte?

Funciona, com um passo a mais. Se o formulário está no HTML que o seu build gera, o atributo já basta. Se ele só aparece depois que o JavaScript roda, escreva action="/__harvis/form/contact" e method="post" no componente — mesmo endpoint, mesmo painel, e quem envia continua sendo o navegador.

As pessoas podem anexar arquivos?

Ainda não. Um campo de arquivo num formulário coletado continua funcionando para o visitante, mas o arquivo não é guardado — só os campos de texto ficam.

O que acontece com os envios se eu excluir o site?

Eles vão junto. Os envios pertencem ao site e não a uma publicação, então publicar de novo mantém tudo e excluir o site apaga de vez. Exporte um CSV antes se quiser guardar.

Coloque um site no ar e teste

Publicar é de graça e leva uns dois segundos. Adicione o atributo a um formulário, publique de novo e mande uma mensagem de teste para você mesmo.

Colocar um site no ar