formulaires

Des formulaires qui marchent sans backend

Ajoute un attribut à un formulaire de ton site statique et chaque réponse arrive dans ton tableau de bord. Pas de JavaScript, pas de widget intégré, pas de service tiers, et rien à signer pour la personne qui le remplit.

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

Trois étapes, dont une consiste à redéployer

Il n'y a pas de générateur de formulaires à apprendre ni d'endpoint à copier. L'attribut, c'est toute la configuration.

  1. 01

    Ajoute l'attribut

    Mets harvis-form sur n'importe quel formulaire de ton HTML. Donne-lui un nom si le site en a plusieurs. Laisse action et method de côté — ils sont remplis au moment où la page est servie.

  2. 02

    Redéploie

    Glisse le dossier à nouveau, ou lance npx harvis. Rien à configurer, aucune clé à coller, aucun interrupteur à activer d'abord dans un tableau de bord.

  3. 03

    Lis ce qui arrive

    Les réponses apparaissent sous ton site dans le tableau de bord, les plus récentes en premier. Ouvre une ligne pour tout lire, ou récupère l'ensemble en CSV.

the markup

Tout ça tient en deux attributs

harvis réécrit le formulaire à sa sortie du serveur, pour que la page que tu as écrite reste la page que tu as écrite.

ce que tu écris
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html">
  <input name="email" type="email" required>
  <button>Send</button>
</form>
ce que reçoivent tes visiteurs
<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>

Les deux attributs

harvis-form
Marque le formulaire comme étant à collecter. La valeur lui donne un nom, pour que plusieurs formulaires d'un même site restent séparés ; sans valeur, la collecte se fait sous « default ».
data-harvis-redirect
Facultatif. La page de ton site vers laquelle envoyer les gens après l'envoi. Tout ce qui pointe hors de ton site est ignoré.
frameworks

Quand c'est JavaScript qui construit le formulaire, écris l'endpoint toi-même

harvis remplit le action au moment où la page quitte le serveur. Un formulaire qui n'existe qu'une fois ton bundle exécuté n'est pas encore dans la page : il n'y a donc rien à remplir, et tu écris ce que la réécriture aurait écrit. Ça reste un post HTML ordinaire, et tout le reste de cette page s'applique toujours.

  • 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
L'endpoint que harvis aurait écrit, plus post. Le dernier segment est le nom du formulaire — minuscules, chiffres et tirets ; tout le reste est collecté sous « default ».
_harvis_redirect
Facultatif, et la version écrite à la main de data-harvis-redirect. Un chemin sur ton propre site ; tout ce qui pointe ailleurs est ignoré.
_harvis_hp
Facultatif, et la seule chose que tu perds en écrivant le formulaire toi-même : le leurre qui attrape les bots. Garde-le hors écran plutôt qu'en display:none, et laisse-le vide — tout ce qui le remplit est jeté.
harvis-form
Ne le mets pas. Cet attribut est une demande faite à harvis d'écrire le action, et tu viens de l'écrire toi-même.

Laisse le navigateur l'envoyer

Pas de preventDefault, pas de fetch. La réponse est une redirection que le navigateur suit tout seul, et c'est ce qui garde le formulaire fonctionnel quand les scripts ne tournent pas.

Prérendu compte comme du JavaScript

Si ton build écrit le formulaire dans le HTML et que ton app le reprend ensuite dans le navigateur, écris quand même les champs — ceux que harvis a insérés ne sont pas dans ton composant, donc l'hydratation peut les jeter.

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

Ce que tu obtiens

Tout ce qui suit est actif par défaut. Il n'y a de page de réglages pour aucun de ces points.

  • Pas de JavaScript

    Le formulaire envoie comme le HTML l'a toujours fait. Ça marche encore avec les scripts bloqués, sur un téléphone lent, et dans un navigateur qui n'affiche que du texte.

  • Le spam est filtré

    Chaque formulaire est servi avec un champ leurre que personne ne peut voir, et tout ce qui le remplit est jeté sans le dire. Des limites de débit plafonnent ce qu'un visiteur, et un site, peuvent envoyer en une heure.

  • Autant de formulaires que tu veux

    Donne-leur des noms — un formulaire de contact et un formulaire d'inscription sur le même site gardent chacun leur liste, et tu peux les filtrer et les exporter séparément.

  • Ta propre page de remerciement

    Pointe le formulaire vers une page que tu as écrite et c'est là que les gens atterrissent après l'envoi. Sans ça, ils voient une simple page de confirmation.

  • Exporte quand tu veux

    Un bouton te donne un CSV avec une colonne pour chaque champ jamais envoyé par qui que ce soit, si bien qu'un formulaire qui a gagné une question en cours de route s'ouvre quand même comme un seul tableau.

  • Personne n'est suivi

    Pas de script de suivi, pas de cookie, aucun tiers dans la page. L'adresse du visiteur est hachée avant d'être stockée, et uniquement pour que la limite de débit puisse distinguer deux envois.

limits

Les limites

Généreuses pour un formulaire de contact, assez serrées pour qu'un script braqué sur ton site ne remplisse pas ta boîte de réception.

Champs par réponse
30
Longueur d'un champ
5 000 caractères
Taille d'une réponse
64 Ko
Réponses par site
60 par heure
Réponses par visiteur
20 par heure
Conservées par site
1 000, les plus récentes d'abord
faq

Questions

Ce qu'on demande avant de mettre un formulaire sur un site statique.

Qu'est-ce qu'un formulaire statique ?

Un formulaire sur un site qui n'a pas de serveur à lui. Normalement, cela veut dire que le formulaire n'a nulle part où envoyer quoi que ce soit, et c'est pour ça que les sites statiques empruntent en général un service de formulaires tiers. harvis sert déjà chaque page de ton site : il peut donc récupérer la réponse au passage.

Faut-il savoir coder ?

Il faut savoir ajouter un mot à une ligne de HTML. Si c'est une IA qui a écrit ton site, demande-lui d'ajouter l'attribut harvis-form au formulaire — elle sait faire, parce que les instructions que harvis publie pour les assistants IA le disent.

Est-ce que ça marche avec JavaScript désactivé ?

Oui. C'est même la raison pour laquelle c'est construit comme ça. Le formulaire fait un envoi HTML ordinaire et le navigateur suit ensuite une redirection, exactement comme les formulaires fonctionnaient avant l'existence de JavaScript.

Puis-je continuer à utiliser mon propre endpoint ou un autre service de formulaires ?

Oui — n'ajoute simplement pas l'attribut. Les formulaires sans harvis-form sont servis exactement comme tu les as écrits, action comprise. Sur un formulaire qui l'a, harvis remplace le action, parce qu'ajouter l'attribut est justement la façon de dire où la réponse doit aller.

Est-ce que ça marche avec React, Vue ou Svelte ?

Oui, avec une étape en plus. Si le formulaire est dans le HTML que produit ton build, l'attribut suffit. S'il n'apparaît qu'une fois JavaScript exécuté, écris plutôt action="/__harvis/form/contact" et method="post" dans le composant — même endpoint, même tableau de bord, et c'est toujours le navigateur qui envoie.

Peut-on joindre des fichiers ?

Pas encore. Un champ de fichier sur un formulaire collecté fonctionne toujours pour le visiteur, mais le fichier n'est pas stocké — seuls les champs texte sont conservés.

Que deviennent les réponses si je supprime le site ?

Elles partent avec lui. Les réponses appartiennent au site et non à un déploiement : redéployer les conserve, supprimer le site les efface pour de bon. Exporte un CSV d'abord si tu veux les garder.

Mets un site en ligne et essaie

Publier est gratuit et prend environ deux secondes. Ajoute l'attribut à un formulaire, redéploie, et envoie-toi un message de test.

Mettre un site en ligne