DEV Community

Cover image for A static site that collects form submissions, in one HTML attribute
Andrius Putna for Harvis

Posted on • Originally published at harvis.dev

A static site that collects form submissions, in one HTML attribute

A static site has no backend. That is the point of one — and it is also why the contact form is the first thing that breaks. The usual answers are a third-party form service with its own signup, a serverless function you now maintain, or a mailto: link nobody clicks.

There is a third option that falls out of how static hosting already works: the host is in the path of every HTML response it serves. It can collect the form itself.

On harvis.dev that is one attribute:

<form harvis-form="contact">
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <button>Send</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Deploy, and submissions show up in the dashboard. No script tag, no API key in the page, no fetch(), no JavaScript at all — the form works with JS disabled, because it is a plain HTML form doing what plain HTML forms have always done.

The page I am describing is live at harvis-forms-example.harvis.dev — submit the form and see where you land. Everything below is what makes that page work.

What actually happens

The rewrite happens on the way out, while the HTML is being served:

  • action and method are replaced with /__harvis/form/contact on your site's own subdomain. Same origin, so there is no CORS, no preflight, and nothing in the page has to know a project id.
  • A honeypot field is inserted. It is positioned off-screen rather than display: none, because a bot that skips hidden inputs is a bot that would otherwise get through. Anything that fills it in gets the success page and is stored nowhere — a bot that can tell it was caught is a bot that tries again differently.
  • data-harvis-redirect="/thanks.html" becomes a hidden field, since the handler never sees your HTML — only what the browser posts. It is re-validated on arrival, and a protocol-relative //somewhere-else is refused.

The reply is a 303, so the browser follows it with a GET and a refresh on the thank-you page cannot post the form twice.

The form name is part of a URL and a dashboard heading, so it has to match [a-z0-9][a-z0-9_-]{0,39}. Anything else — including a bare harvis-form with no value — collects under default. Two forms on one page, two named inboxes:

<form harvis-form="contact" data-harvis-redirect="/thanks.html"></form>
<form harvis-form="newsletter"></form>
Enter fullscreen mode Exit fullscreen mode

The second one has no redirect, so it lands on a branded thank-you page the host serves.

The one case where the attribute does nothing

The rewrite runs over the response body, so it only sees markup that is already in the file. A form that React, Vue or Svelte mounts at runtime has no <form> in the served HTML — the marker never matches and nothing is rewritten. Write the endpoint yourself instead:

<form action="/__harvis/form/contact" method="post">
  <input type="hidden" name="_harvis_redirect" value="/thanks" />
  {/* …your fields… */}
</form>
Enter fullscreen mode Exit fullscreen mode

The receiving side does not care who wrote those fields. Drop the harvis-form attribute when you do this, or a prerendered page gets rewritten twice and ends up with two honeypots.

Try it

The page: https://harvis-forms-example.harvis.dev/

The complete source — two forms, the deploy script, the workflow — is here:

https://github.com/harvis-io/solutions/tree/main/forms-example

Three files of site, no build step, no dependencies. Clone it, run HARVIS_API_KEY=hvs_… ./deploy.sh public, and you have a static page with a working contact form about two seconds later.

Here is the served HTML of that live page, which is the shortest version of this whole post:

<!-- what is in the repo -->
<form harvis-form="contact" data-harvis-redirect="/thanks.html">

<!-- what the browser receives -->
<form harvis-form="contact" data-harvis-redirect="/thanks.html"
      action="/__harvis/form/contact" method="post">
Enter fullscreen mode Exit fullscreen mode

Top comments (0)