integrations · framework

A Nuxt contact form that outlives nuxt generate.

Nuxt ships a server. Drop a file in server/api/, and Nitro gives you a real endpoint with no extra setup, which makes the obvious contact-form answer look easy. Then you run nuxt generate to prerender the site onto a CDN, and the obvious answer disappears: a fully static Nuxt build has no Nitro runtime, so server/api/contact.post.ts is compiled and then never deployed anywhere that can run it.

This is the trap that catches people who picked Nuxt precisely because it can go static. Point the form at a mailcontact endpoint and the contact page stops caring: it works identically under nuxt build with a Node server, under nuxt generate on a CDN, and on the preset your host swaps you onto next quarter.

recommended setup

Use the MailContact script with your own design.

Add this HTML to your site, or use the form builder. Replace the form link with yours. The hosted script injects the hidden check, handles sending and automatic verification, and preserves the draft on errors. You control the HTML and CSS. Free forms display a visible “Processed by mailcontact.app” link; Pro and Plus do not. Feedback follows the browser’s language; set data-mailcontact-language="fr" on the form to force French.

contact.html
<form action="https://mailcontact.app/api/forms/your-form-id" method="POST" data-mailcontact-form>
  <fieldset>
    <legend>Contact us</legend>
    <label>
      Name (optional)
      <input name="name" type="text" autocomplete="name" maxlength="200" />
    </label>
    <label>
      Email
      <input name="email" type="email" autocomplete="email" maxlength="320" required />
    </label>
    <label>
      Message
      <textarea name="message" rows="5" maxlength="5000" required></textarea>
    </label>
    <noscript>
    <div aria-hidden="true" style="position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;">
      <label>Website
        <input name="company_website" type="text" tabindex="-1" autocomplete="off" />
      </label>
    </div>
    </noscript>
    <button type="submit">Send message</button>
  </fieldset>
  <p data-contact-status role="status" aria-live="polite" aria-atomic="true"></p>
</form>
<script defer src="https://mailcontact.app/forms/v1.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

For a component with its own submit handler, load the same script and use MailContact.prepare(form) and MailContact.send() instead of the automatic marker. See the custom interface reference for loading, feedback and Free attribution.

the snippet

Alternative: a direct integration for Nuxt.

This example posts directly to the API. Keep the required company_website field present and empty. Native posts open JSON on success or a verification page when needed. Custom scripts without verification support send suspicious messages to Spam for review; strong spam is also held there without notifications or quota usage. A success response confirms reception, including review, or a silent discard of a filled check. See spam protection for the full behavior.

pages/contact.vue
<script setup>
// No server/api/contact.post.ts. Nothing here needs Nitro at runtime,
// so `nuxt generate` keeps this page fully working.
const sent = ref(false);

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

<template>
  <p v-if="sent">Thanks. We'll be in touch.</p>

  <form v-else @submit.prevent="submit">
    <input name="name" placeholder="Your name" required />
    <input name="email" type="email" placeholder="[email protected]" required />
    <textarea name="message" placeholder="What's on your mind?" required />
    <input name="company_website" type="text" tabindex="-1" autocomplete="off"
      style="position:absolute;left:-10000px" aria-hidden="true" />

    <button>Send</button>
  </form>
</template>

how it works

Three steps, start to finish.

01

Create an inbox

One inbox per project. The form endpoint and the project email address are live before you add the page.

02

Add the page

Paste it into pages/contact.vue and swap the endpoint id. There is no server/ file to add, no runtimeConfig entry, and no private key to keep out of the client bundle.

03

Get notified

Add a Discord, Slack, email, or webhook channel, and submissions reach you the moment they arrive.

why mailcontact

Built to pair well with Nuxt.

Static output stays on the table

A Nitro route is the one thing that stops nuxt generate from being enough. Keep the form external and you keep the option to deploy the whole site as files, whenever you want it.

No mail key in runtimeConfig

The DIY version needs an API key for a mail provider, which means a secret, a place to store it, and a per-host config to get wrong. There is no key here to leak or rotate.

The preset stops mattering

Nitro presets differ per host, and a server route that works on one can behave differently on another. Markup pointing at an external URL behaves the same everywhere, because it never touches the preset.

faq

Nuxt + mailcontact, answered

Can I use a Nuxt server route instead?

Yes, if you deploy a server. server/api/contact.post.ts works well under nuxt build with a Node or serverless preset. It stops working under nuxt generate, because a prerendered site has no Nitro runtime to receive the POST. If static output matters to you, keep the form off the server graph.

Does this work with Nuxt 3 and Nuxt 4?

Yes. The example only uses ref, $fetch and @submit.prevent, all of which are stable across both. You can also swap $fetch for the native fetch: the payload is a FormData either way, so nothing here depends on Nuxt's fetch layer.

Should I use useFetch here?

No. useFetch is built for data you need during render, and it dedupes and caches, which is wrong for a one-shot POST triggered by a click. Call $fetch (or fetch) directly inside the submit handler.

How do I keep spam out without a captcha?

The company_website check must be present and empty. Filling it silently discards the submission, but a capable bot can skip it. MailContact also applies server-side limits, content and campaign checks. The hosted script handles automatic verification when needed; strong spam is held for review without notifications or quota usage.

related

Also works with

One inbox for every Nuxt project you ship.

Forms and a real email address, together, with notifications where you live. Start with a free account.

Create your free form

No credit card needed. Free includes one project and 50 form messages per month.