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
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. Withnuxt generate, the value is baked in at build time, so set the variable in the build environment.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 ofapp/components/. ignoreResponseError: truestops$fetchthrowing on4xx, so the error envelope comes back as data. Only network failures reachcatch.@submit.preventonly exists once the app has hydrated. Before that, the browser posts toactionand follows_redirect.
- On Nuxt 3, put it in
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 runs | Add to Allowed domains |
|---|---|
nuxt dev | localhost. It matches http://localhost:3000: Ikiform compares the host name only, never the port. |
| Your domain | example.com, which also covers www.example.com |
| Vercel | your-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. |
| Netlify | your-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 Pages | your-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.apporpages.devon its own. Every site on that host would match, including other people's.
Test checklist
- Run
nuxt devwithlocalhostin allowed domains and submit. The thanks message should replace the form. - Turn JavaScript off in your browser's dev tools, reload and submit. You should land on Ikiform's thank-you page locally, or
/thankson yourhttps:domain. - Submit an invalid email with fields defined and confirm the message appears under the field.
- Run
nuxt generate, deploy the output, and submit from the preview host.
Troubleshooting
| What you see | Why | Fix |
|---|---|---|
FetchError: [POST] "https://ikiform.com/f/...": 422 | ignoreResponseError is missing, so $fetch throws on errors | Add ignoreResponseError: true. |
404 not_found, and the action ends in /f/undefined | The form id wasn't set when the site was built | Set NUXT_PUBLIC_IKIFORM_FORM_ID in the build environment, or the default in nuxt.config.ts. |
| Without JavaScript, nothing happens | The page is client-rendered with ssr: false | Enable server rendering or prerender the page. |
Submissions arrive under the spam filter with origin_mismatch | The page that posted isn't on the form's allowed domains | Add the host from the table above. With Reject chosen, the same posts get 403 origin_not_allowed instead. |