<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: formhook.app</title>
    <description>The latest articles on DEV Community by formhook.app (@formhook).</description>
    <link>https://dev.to/formhook</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4011105%2Fdc461823-80cb-437e-98f1-6a9b8654d929.jpg</url>
      <title>DEV Community: formhook.app</title>
      <link>https://dev.to/formhook</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/formhook"/>
    <language>en</language>
    <item>
      <title>Add a working contact form to your static site in 30 seconds (no backend, no serverless)</title>
      <dc:creator>formhook.app</dc:creator>
      <pubDate>Wed, 01 Jul 2026 15:41:25 +0000</pubDate>
      <link>https://dev.to/formhook/add-a-working-contact-form-to-your-static-site-in-30-seconds-no-backend-no-serverless-2bmb</link>
      <guid>https://dev.to/formhook/add-a-working-contact-form-to-your-static-site-in-30-seconds-no-backend-no-serverless-2bmb</guid>
      <description>&lt;p&gt;Static sites are having a moment again. Astro, plain HTML, Next.js static exports, Hugo - fast, cheap to host, nothing to patch at 2 AM. There's exactly one feature that keeps dragging people back to servers: the contact form.&lt;/p&gt;

&lt;p&gt;The usual escalation goes like this. You ship a beautiful static site, the client asks "where do the enquiries go?", and suddenly you're evaluating serverless functions, SMTP credentials, and spam filtering for what should be three input fields.&lt;/p&gt;

&lt;p&gt;Here's the entire integration with a form backend instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://formhook.app/f/your-form-id"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No JavaScript required. The form POSTs to an endpoint, the endpoint stores the submission, and you get a push notification on your phone. Works identically whether the page is hand-written HTML on a €3 VPS or an Astro build on Cloudflare Pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same thing in Astro and Next.js
&lt;/h2&gt;

&lt;p&gt;Because it's a plain HTML form, framework doesn't matter - but if you want the AJAX version so the page doesn't navigate away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://formhook.app/f/your-form-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrap that in whatever component model you use. There's no SDK to install, which I consider a feature: one less dependency to update, and the integration survives every framework migration you'll ever do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about spam?
&lt;/h2&gt;

&lt;p&gt;This is where DIY solutions usually fall over. A naked endpoint gets hammered by bots within days. A decent form backend gives you layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Honeypot fields&lt;/strong&gt; - invisible inputs that only bots fill in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; - per-IP throttling at the endpoint&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Turnstile&lt;/strong&gt; - when you need a real challenge, without making humans click traffic lights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With FormHook the first two are on by default and Turnstile is a checkbox. You can see all three working in the live demo on the homepage - submit the form and watch it land in the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before you pick any form backend
&lt;/h2&gt;

&lt;p&gt;I build static sites for clients, and I built FormHook (disclosure: it's my product) after getting burned by the fine print on existing services. Whatever you choose, check these four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What's the free tier actually for?&lt;/strong&gt; Some providers position free plans explicitly for testing and development, not production. If you're wiring up a real client site, read that line carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How long are submissions kept?&lt;/strong&gt; Retention limits are the quietest gotcha in this category. A lead that auto-deletes after 30 days is a lead your client loses. FormHook keeps submissions forever on every tier, including free - but whoever you pick, find their retention policy in writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where does the data live?&lt;/strong&gt; If you or your clients are in the EU, a US-hosted form processor means your contact form depends on the EU–US Data Privacy Framework staying valid. EU-resident hosting makes that whole question disappear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you export?&lt;/strong&gt; If leaving requires begging support for a data dump, don't enter.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The 30 seconds, measured
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Sign up, create a form, copy the endpoint (≈20 seconds)&lt;/li&gt;
&lt;li&gt;Paste it into your &lt;code&gt;action&lt;/code&gt; attribute (≈10 seconds)&lt;/li&gt;
&lt;li&gt;Deploy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free tier (250 submissions/month) covers a typical portfolio or small business site with room to spare. And because the integration is one attribute, switching providers later - to us or away from us - is a one-line diff. That's how this category should work.&lt;/p&gt;

&lt;p&gt;Questions about static site forms? Happy to answer in the comments - I've wired these into everything from hand-rolled HTML to Next.js 15 static exports.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>tutorial</category>
      <category>staticsites</category>
    </item>
  </channel>
</rss>
