integrations · framework

A SvelteKit contact form that survives adapter-static.

SvelteKit has a genuinely good answer for forms: a form action in +page.server.js, progressive enhancement with use:enhance, the whole thing works without JavaScript. It has exactly one catch, and it is the one that bites the people who reach for SvelteKit in the first place. Form actions need somewhere to run. Switch to adapter-static to put your site on GitHub Pages or a CDN and every action in the project stops existing, because there is no server left to receive the POST.

So you either keep a server alive to handle three fields, or you point the form somewhere else. A mailcontact endpoint is somewhere else: submissions land in your project inbox, the build stays fully static, and the contact page behaves the same on adapter-static, adapter-node, or adapter-vercel.

the snippet

A working form for SvelteKit, in one paste.

Point the form at your project's mailcontact endpoint. Submissions land in your project inbox. No server, no API route, no email service to wire up.

src/routes/contact/+page.svelte
<script>
  // No +page.server.js. No actions. No adapter requirement.
  let sent = $state(false);

  async function submit(event) {
    await fetch("https://mailcontact.app/api/forms/your-form-id", {
      method: "POST",
      body: new FormData(event.currentTarget),
    });
    sent = true;
  }
</script>

{#if sent}
  <p>Thanks. We'll be in touch.</p>
{:else}
  <form onsubmit={(e) => { e.preventDefault(); submit(e); }}>
    <input name="name" placeholder="Your name" required />
    <input name="email" type="email" placeholder="you@email.com" required />
    <textarea name="message" placeholder="What's on your mind?" required></textarea>
    <input name="_gotcha" tabindex="-1" autocomplete="off" hidden />

    <button>Send</button>
  </form>
{/if}

how it works

Three steps, start to finish.

01

Create an inbox

One inbox per project, with a form endpoint and a project email address ready before you touch the routes directory.

02

Add the route

Paste the block into src/routes/contact/+page.svelte. Note what is missing: no sibling +page.server.js, so nothing here forces a server adapter on the rest of your build.

03

Get notified

Wire a Discord, Slack, email, or webhook channel and every submission pings you there.

why mailcontact

Built to pair well with SvelteKit.

Your adapter choice stays free

The moment a route needs an action, adapter-static is off the table for the whole project. Keeping the contact form out of the server graph means you can still prerender everything and host it as files.

No server for three fields

adapter-node for a contact page means a process to run, patch, and pay for. That is a lot of infrastructure to justify with a form most sites receive twice a month.

One inbox per side project

SvelteKit is what a lot of people reach for on their fourth idea of the year. Each of those gets its own inbox and its own address rather than all funnelling into your personal mail.

faq

SvelteKit + mailcontact, answered

Why can't I just use a SvelteKit form action?

You can, as long as you keep a server. Actions are server-side handlers, so they need adapter-node, adapter-vercel, or another runtime adapter. With adapter-static there is nothing to POST to and the action is simply not deployed. If your site is otherwise fully static, a contact form is a poor reason to give that up.

Does this work with use:enhance?

use:enhance is built to progressively enhance a SvelteKit action, so it expects an internal action response and does not apply here. The handler above does the equivalent job in about the same number of lines: intercept the submit, post the FormData, swap in your success state.

Can I keep it working without JavaScript?

Yes. Drop the script and give the form a plain action attribute pointing at the endpoint plus method="POST". The browser does a native form post with no client code at all. Today that shows the raw JSON response; a configurable redirect is coming with the launch.

Does this work in Svelte 4, or only with runes?

Both. The example uses $state and the onsubmit property from Svelte 5, but nothing here depends on runes. On Svelte 4, use let sent = false and on:submit|preventDefault instead; the fetch call is identical.

related

Also works with

One inbox for every SvelteKit project you ship.

Forms and a real email address, together, with notifications where you live. Launching soon. Be the first to know.

No spam. It's kind of our thing.