Form ID

SvelteKit

A prerendered SvelteKit page whose form posts straight to Ikiform, enhanced with fetch once JavaScript loads. Works with adapter-static.

This page needs no server: the form's action is your Ikiform endpoint, so it works on adapter-static and any static host. Without JavaScript it posts and redirects like plain HTML; with JavaScript it posts with fetch and shows the result in place. To post through a form action with a secret key instead, see Svelte.

Why not use:enhance

SvelteKit's use:enhance only works with method="POST" forms that point to actions defined in a +page.server.js file. Ikiform is an outside URL, so the page uses a small onsubmit handler instead. The progressive enhancement is the same: the form works first, the script improves it.

Set it up

  1. Add public environment variables

    .env
    PUBLIC_IKIFORM_FORM_ID=frm_xxxxxxxxxxxxxxxx
    PUBLIC_SITE_URL=https://example.com

    PUBLIC_SITE_URL is for the no-JavaScript redirect. While prerendering, page.url.origin is http://sveltekit-prerender, not your domain, so the redirect can't be built from it.

  2. Prerender the page

    src/routes/contact/+page.js
    export const prerender = true;
  3. Add the form

    src/routes/contact/+page.svelte
    <script>
      import { PUBLIC_IKIFORM_FORM_ID, PUBLIC_SITE_URL } from "$env/static/public";
    
      const endpoint = `https://ikiform.com/f/${PUBLIC_IKIFORM_FORM_ID}`;
    
      let status = $state("idle");
      let errors = $state({});
      let message = $state("");
    
      async function onsubmit(event) {
        event.preventDefault();
        status = "sending";
        try {
          const response = await fetch(endpoint, {
            method: "POST",
            headers: { Accept: "application/json" },
            body: new FormData(event.currentTarget),
          });
          const result = await response.json();
          if (result.ok) {
            status = "sent";
            return;
          }
          errors = result.error.fields ?? {};
          message = result.error.message;
        } catch {
          message = "Couldn't reach the server. Try again.";
        }
        status = "error";
      }
    </script>
    
    {#if status === "sent"}
      <p>Thanks, we got it.</p>
    {:else}
      <form action={endpoint} method="POST" {onsubmit}>
        <label>
          Email
          <input name="email" type="email" autocomplete="email" required aria-invalid={errors.email ? "true" : undefined} />
        </label>
        {#if errors.email}<p>{errors.email[0]}</p>{/if}
        <label>
          Message
          <textarea name="message" rows="5"></textarea>
        </label>
        <input type="hidden" name="_redirect" value={`${PUBLIC_SITE_URL}/contact/thanks`} />
        <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
        {#if status === "error" && !errors.email}<p role="alert">{message}</p>{/if}
        <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
      </form>
    {/if}

    Add a src/routes/contact/thanks/+page.svelte for the redirect target.

If you use SvelteKit's CSP

With kit.csp configured, allow Ikiform for both the fetch post and the plain form post. Otherwise the browser blocks one or the other.

svelte.config.js
const config = {
  kit: {
    csp: {
      directives: {
        "connect-src": ["self", "https://ikiform.com"],
        "form-action": ["self", "https://ikiform.com"],
      },
    },
  },
};

export default config;

Allowed domains

List production plus your preview hosts.

Where the form runsAdd to Allowed domains
vite devlocalhost. It matches http://localhost:5173: 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.
Cloudflare Pagesyour-project.pages.dev. Preview URLs such as abc123.your-project.pages.dev are subdomains of it, so they already match.
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.
  • Never add vercel.app, pages.dev or netlify.app on its own. Every site on that host would match, including other people's.

Test checklist

  1. Run the dev server with localhost in allowed domains and submit. The thanks message should replace the form.
  2. Run vite build and check the output contains contact.html with the form already in it.
  3. Turn JavaScript off, reload and submit. On your https: domain you should land on /contact/thanks.
  4. Submit from a preview deploy and confirm it isn't marked spam.

Troubleshooting

What you seeWhyFix
The build fails saying PUBLIC_IKIFORM_FORM_ID isn't exported by $env/static/publicThe variable isn't set where the build runsAdd it to .env locally and to your host's build settings.
Nothing happens on submit, and the console shows a CORS error about the x-sveltekit-action headeruse:enhance on a form that posts outside your appRemove it and keep the onsubmit handler.
The browser refuses to send the form because of form-action or connect-srckit.csp doesn't list IkiformAdd https://ikiform.com to both directives.
Without JavaScript you land on Ikiform's thank-you pagePUBLIC_SITE_URL is http:, or the domain isn't in allowed domainsUse your https: address and add the domain.
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.