Form ID

Nuxt

A Nuxt component that posts as a plain form before hydration and with $fetch after, with the form id in runtime config. Works with nuxt generate.

Nuxt renders the form on the server, so the HTML that reaches the browser is already a working form. Until the app hydrates it posts like any HTML form; after that, the component takes over and shows the result in place. Nothing runs on your server, so it also works as a static site built with nuxt generate.

To post from a Nuxt server route with a secret key instead, see Vue.

Set it up

  1. Put the form id in runtime config

    nuxt.config.ts
    export default defineNuxtConfig({
      runtimeConfig: {
        public: {
          ikiformFormId: "frm_xxxxxxxxxxxxxxxx",
        },
      },
    });

    Override it per environment with NUXT_PUBLIC_IKIFORM_FORM_ID. With nuxt generate, the value is baked in at build time, so set the variable in the build environment.

  2. Add the component

    app/components/ContactForm.vue
    <script setup lang="ts">
    type Result =
      | { ok: true; id: string }
      | { ok: false; error: { message: string; fields?: Record<string, string[]> } };
    
    const config = useRuntimeConfig();
    const endpoint = `https://ikiform.com/f/${config.public.ikiformFormId}`;
    
    const status = ref<"idle" | "sending" | "sent" | "error">("idle");
    const errors = ref<Record<string, string[]>>({});
    const message = ref("");
    
    async function onSubmit(event: Event) {
      status.value = "sending";
      const result = await $fetch<Result>(endpoint, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(event.currentTarget as HTMLFormElement),
        ignoreResponseError: true,
      }).catch(() => null);
      if (result?.ok) {
        status.value = "sent";
        return;
      }
      errors.value = result?.error.fields ?? {};
      message.value = result?.error.message ?? "Couldn't reach the server. Try again.";
      status.value = "error";
    }
    </script>
    
    <template>
      <p v-if="status === 'sent'">Thanks, we got it.</p>
      <form v-else :action="endpoint" method="post" @submit.prevent="onSubmit">
        <label>
          Email
          <input name="email" type="email" autocomplete="email" required />
        </label>
        <p v-if="errors.email">{{ errors.email[0] }}</p>
        <label>
          Message
          <textarea name="message" rows="5" />
        </label>
        <input type="hidden" name="_redirect" value="https://example.com/thanks" />
        <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
        <p v-if="status === 'error' && !errors.email" role="alert">{{ message }}</p>
        <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send" }}</button>
      </form>
    </template>
    • On Nuxt 3, put it in components/ instead of app/components/.
    • ignoreResponseError: true stops $fetch throwing on 4xx, so the error envelope comes back as data. Only network failures reach catch.
    • @submit.prevent only exists once the app has hydrated. Before that, the browser posts to action and follows _redirect.
  3. Use it on a page

    app/pages/contact.vue
    <template>
      <ContactForm />
    </template>

The fallback needs server rendering or prerendering. With ssr: false, the page is empty until JavaScript runs, so there is no form to post without it.

Allowed domains

List production plus the preview hosts of wherever you deploy.

Where the form runsAdd to Allowed domains
nuxt devlocalhost. It matches http://localhost:3000: Ikiform compares the host name only, never the port.
Your domainexample.com, which also covers www.example.com
Vercelyour-project.vercel.app. Preview URLs such as your-project-git-branch-team.vercel.app don't match it; add the exact preview host while you test one.
Netlifyyour-site.netlify.app. Deploy previews (deploy-preview-12--your-site.netlify.app) and branch deploys (branch--your-site.netlify.app) are not subdomains of it, so add the exact preview host while you test one.
Cloudflare Pagesyour-project.pages.dev. Preview URLs such as abc123.your-project.pages.dev are subdomains of it, so they already match.
  • Never add vercel.app, netlify.app or pages.dev on its own. Every site on that host would match, including other people's.

Test checklist

  1. Run nuxt dev with localhost in allowed domains and submit. The thanks message should replace the form.
  2. Turn JavaScript off in your browser's dev tools, reload and submit. You should land on Ikiform's thank-you page locally, or /thanks on your https: domain.
  3. Submit an invalid email with fields defined and confirm the message appears under the field.
  4. Run nuxt generate, deploy the output, and submit from the preview host.

Troubleshooting

What you seeWhyFix
FetchError: [POST] "https://ikiform.com/f/...": 422ignoreResponseError is missing, so $fetch throws on errorsAdd ignoreResponseError: true.
404 not_found, and the action ends in /f/undefinedThe form id wasn't set when the site was builtSet NUXT_PUBLIC_IKIFORM_FORM_ID in the build environment, or the default in nuxt.config.ts.
Without JavaScript, nothing happensThe page is client-rendered with ssr: falseEnable server rendering or prerender the page.
Submissions arrive under the spam filter with origin_mismatchThe page that posted isn't on the form's allowed domainsAdd the host from the table above. With Reject chosen, the same posts get 403 origin_not_allowed instead.