# Un formulaire d'inscription à la newsletter sans l'embed de Mailchimp

> Créez un formulaire d'inscription à la newsletter avec votre propre markup, enregistrez le consentement, bloquez les inscriptions de bots et transmettez chaque abonné à Kit, MailerLite, beehiiv ou n'importe quel outil.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/newsletter-signup

Écrivez le formulaire d'inscription dans votre propre HTML et faites-le pointer vers un backend de formulaires au lieu de coller le code d'embed de votre outil d'e-mailing. Le backend valide l'adresse et la case de consentement, filtre les bots et envoie un webhook signé, et un petit relais ajoute l'abonné via l'API de votre outil d'e-mailing. L'outil envoie ensuite l'e-mail de confirmation, la newsletter et les liens de désinscription : changer d'outil plus tard revient à changer le relais, pas chaque formulaire.

Chaque outil d'e-mailing vous donne un embed : un bloc de markup généré à coller dans votre page. Ça fonctionne, mais le formulaire appartient alors à l'outil. Il envoie directement chez ce fournisseur, il porte son markup et souvent ses styles et ses scripts, et le jour où vous passez à un autre outil, vous traquez chaque copie sur tout votre site. Ce guide construit un formulaire d'inscription qui vous appartient, dans le markup et le framework que vous utilisez déjà, et le relie à l'outil qui envoie votre newsletter.

## Le trajet d'une inscription

Il y a deux tâches ici, et elles ont leur place à des endroits différents. Recueillir une inscription est une tâche de formulaire : valider l'adresse, vérifier la case de consentement, bloquer les bots, enregistrer le moment. Envoyer une newsletter est une tâche d'e-mailing : confirmer l'adresse, envoyer les numéros, gérer les désinscriptions et les rebonds. Ikiform fait la première. Votre outil d'e-mailing fait la seconde. Un webhook signé les relie.

Schéma : Comment une inscription à la newsletter voyage de votre page à votre outil d'e-mailing

1. Un lecteur remplit le formulaire d'inscription de votre page, qui envoie à votre endpoint Ikiform.
2. Ikiform vérifie l'adresse, la case de consentement obligatoire, le honeypot, le score de spam et la limite de débit.
3. L'inscription acceptée est stockée dans le tableau de bord avec son horodatage, son origine et son referrer.
4. Ikiform peut vous écrire à chaque inscription. Il n'écrit jamais au lecteur.
5. Ikiform envoie un webhook submission.created signé.
6. Un relais vérifie la signature et ajoute le lecteur à votre outil d'e-mailing, qui envoie la confirmation puis chaque numéro.

Le relais est la seule partie qui sait quel outil d'e-mailing vous utilisez. Le formulaire, le filtrage du spam et l'enregistrement stocké ne changent pas quand l'outil change.

## Quatre façons de recueillir des inscriptions à une newsletter

| Approche | Points forts | Compromis |
| --- | --- | --- |
| L'embed de l'outil d'e-mailing, de Mailchimp, Kit, MailerLite ou beehiiv | Rien à faire tourner. La confirmation, les tags et l'e-mail de bienvenue se configurent dans les réglages de l'outil. | Le markup et le comportement sont ceux de l'outil. Mailchimp précise que ses formulaires intégrés héritent des styles de votre site et peuvent donc avoir un autre rendu sur votre page, et activer son reCAPTCHA sur un formulaire existant oblige à générer et coller un nouveau code. Chaque copie est liée à un seul fournisseur. |
| Votre propre route serveur qui appelle l'API de l'outil | Contrôle total du formulaire et de la requête, avec la clé d'API gardée sur votre serveur. | Vous construisez vous-même la validation, le filtrage des bots, les limites de débit et l'enregistrement du consentement, et il vous faut un serveur même si le site est statique. |
| Un créateur de formulaires hébergé plus une automatisation, comme [Typeform](https://ikiform.com/alternatives/typeform) avec Zapier | Pas de code, et l'outil d'automatisation connaît tous les fournisseurs d'e-mailing. | Une iframe qui ne ressemble pas à votre page, plus deux abonnements. Webhooks et personnalisation de la marque dépendent de l'offre. |
| Un backend de formulaires, comme Ikiform, plus un relais | Votre propre markup sur n'importe quel site, filtrage du spam, un enregistrement du consentement et des webhooks signés, sans serveur pour le formulaire. | Ikiform n'envoie aucun e-mail au lecteur. La confirmation et chaque newsletter viennent de votre outil d'e-mailing, via un petit relais ou une automatisation. |

Un seul site, un seul outil et son embed vous conviennent ? Gardez-les. Construisez le vôtre quand le formulaire doit suivre votre design, s'étend sur plusieurs sites ou risque de survivre à l'outil.

## Le formulaire

Un bon formulaire de newsletter est petit : un e-mail, un prénom facultatif pour la salutation, et une case de consentement dont le libellé est la promesse que vous faites. Un input masqué `source` enregistre l'endroit du site où l'inscription a eu lieu. Choisissez votre stack ; ce choix s'applique à tous les exemples de code de ces guides.

**HTML**

Collez le formulaire dans votre pied de page, votre barre latérale ou à la fin d'un article. Il envoie de façon classique et arrive sur votre propre page de remerciement : il fonctionne donc sans JavaScript.

`footer.html`:

```html
<form id="newsletter" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="nl-email">Email</label>
  <input id="nl-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" />

  <label for="nl-name">First name (optional)</label>
  <input id="nl-name" name="first_name" type="text" maxlength="80" autocomplete="given-name" />

  <label>
    <input name="consent" type="checkbox" value="true" required />
    Email me the newsletter. I can unsubscribe at any time.
  </label>

  <input type="hidden" name="source" value="footer" />
  <input type="hidden" name="_redirect" value="https://example.com/subscribed" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />

  <button>Subscribe</button>
  <p id="nl-status" role="status" hidden></p>
</form>
```

Pour garder le lecteur sur l'article, ajoutez ce script après le formulaire. Il envoie avec `fetch`, remplace le formulaire par une ligne qui l'invite à vérifier sa boîte de réception, et affiche le message du serveur pour l'e-mail ou la case.

`newsletter.js`:

```js
const form = document.querySelector("#newsletter");
const status = document.querySelector("#nl-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const body = new FormData(form);
  button.disabled = true;
  status.hidden = false;
  status.textContent = "Subscribing";
  try {
    const response = await fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      form.replaceChildren(status);
      status.textContent = "Almost done. Open the email we sent you and confirm your address.";
      return;
    }
    const fields = result.error.fields ?? {};
    status.textContent = fields.email?.[0] ?? fields.consent?.[0] ?? result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

**Next.js**

Une server action transmet l'inscription avec votre clé secrète. Elle renvoie les erreurs par champ : le message sur l'e-mail se place sous l'input e-mail, et celui sur le consentement sous la case. Le composant accepte une prop `source`, pour que le pied de page et un encart dans l'article enregistrent des valeurs différentes.

`app/newsletter/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface SignupState {
  done: boolean;
  errors: { email?: string; consent?: string; form?: string };
  email: string;
}

export async function subscribe(_previous: SignupState, formData: FormData): Promise<SignupState> {
  const email = String(formData.get("email") ?? "").trim();
  const consent = formData.get("consent") === "true";
  if (!consent) return { done: false, errors: { consent: "Tick the box so we know you want the emails." }, email };

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: (await headers()).get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      first_name: String(formData.get("first_name") ?? "").trim() || undefined,
      consent,
      source: String(formData.get("source") ?? "footer"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { done: true, errors: {}, email };
  const fields = result?.error?.fields ?? {};
  return {
    done: false,
    errors: {
      email: fields.email?.[0],
      consent: fields.consent?.[0],
      form: fields.email || fields.consent ? undefined : (result?.error?.message ?? "Couldn't subscribe. Try again."),
    },
    email,
  };
}
```

`app/newsletter/newsletter-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type SignupState, subscribe } from "./actions";

const initial: SignupState = { done: false, errors: {}, email: "" };

export function NewsletterForm({ source = "footer" }: { source?: string }) {
  const [state, action, pending] = useActionState(subscribe, initial);

  if (state.done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form action={action} noValidate>
      <label htmlFor="nl-email">Email</label>
      <input
        id="nl-email"
        name="email"
        type="email"
        required
        autoComplete="email"
        defaultValue={state.email}
        aria-invalid={Boolean(state.errors.email)}
        aria-describedby="nl-email-error"
      />
      <p id="nl-email-error">{state.errors.email}</p>
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} autoComplete="given-name" />
      <label>
        <input name="consent" type="checkbox" value="true" aria-describedby="nl-consent-error" />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      <p id="nl-consent-error">{state.errors.consent}</p>
      <input type="hidden" name="source" value={source} />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.errors.form ? <p role="alert">{state.errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

Pour aller plus loin avec Next.js, lisez [Créer un formulaire d'inscription à la newsletter avec Next.js](https://ikiform.com/fr/guides/newsletter-signup/nextjs.md).

**Astro**

Une Astro Action avec `accept: "form"` interprète la case comme un booléen et l'e-mail avec `z.email()`. `isInputError` distingue une adresse invalide d'un envoi refusé. Les actions de formulaire ont besoin d'une page à la demande : ajoutez donc un adaptateur et mettez `export const prerender = false` dans chaque page qui affiche le composant ; cet export n'a aucun effet dans un composant.

`src/actions/index.ts`:

```ts
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";

export const server = {
  subscribe: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      first_name: z.string().max(80).nullish(),
      consent: z.boolean(),
      source: z.string().default("footer"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      if (!input.consent) {
        throw new ActionError({ code: "BAD_REQUEST", message: "Tick the box so we know you want the emails." });
      }
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify({ ...input, first_name: input.first_name ?? undefined }),
      });
      const result = await response.json();
      if (!result.ok) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: result.error.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { subscribed: true };
    },
  }),
};
```

`src/components/NewsletterForm.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

const result = Astro.getActionResult(actions.subscribe);
const fields = isInputError(result?.error) ? result.error.fields : {};
---

{result?.data?.subscribed ? (
  <p role="status">Almost done. Open the email we sent you and confirm your address.</p>
) : (
  <form method="POST" action={actions.subscribe}>
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    {fields.email && <p>Enter a valid email address.</p>}
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
    <button>Subscribe</button>
  </form>
)}
```

Pour aller plus loin avec Astro, lisez [Créer un formulaire d'inscription à la newsletter avec Astro](https://ikiform.com/fr/guides/newsletter-signup/astro.md).

**TanStack Start**

Une fonction POST `createServerFn` transforme le `FormData` en JSON dans `.validator()` et renvoie les erreurs de champ comme données au lieu de lever une exception, pour que le composant place chaque message à côté de son input.

`src/components/newsletter-form.tsx`:

```tsx
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

type Errors = { email?: string; consent?: string; form?: string };

const subscribe = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      email: String(data.get("email") ?? "").trim(),
      first_name: String(data.get("first_name") ?? "").trim() || undefined,
      consent: data.get("consent") === "true",
      source: String(data.get("source") ?? "footer"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }): Promise<{ ok: boolean; errors: Errors }> => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, errors: {} };
    const fields = result.error.fields ?? {};
    return {
      ok: false,
      errors: { email: fields.email?.[0], consent: fields.consent?.[0], form: result.error.message },
    };
  });

export function NewsletterForm() {
  const send = useServerFn(subscribe);
  const [done, setDone] = useState(false);
  const [errors, setErrors] = useState<Errors>({});
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      const result = await send({ data: new FormData(event.currentTarget) });
      setDone(result.ok);
      setErrors(result.errors);
    } catch {
      setErrors({ form: "Couldn't subscribe. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {errors.email ? <p>{errors.email}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {errors.consent ? <p>{errors.consent}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form && !errors.email && !errors.consent ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

Pour aller plus loin avec TanStack Start, lisez [Créer un formulaire d'inscription à la newsletter avec TanStack Start](https://ikiform.com/fr/guides/newsletter-signup/tanstack-start.md).

**React Router**

Un formulaire de pied de page ne devrait pas quitter la page en cours de lecture. Placez l'action dans une resource route à `/newsletter` et envoyez-y avec `useFetcher`, qui soumet sans navigation et garde le résultat sur la page courante.

`app/routes/newsletter.tsx`:

```ts
import { data } from "react-router";
import type { Route } from "./+types/newsletter";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: request.headers.get("origin") ?? "",
    },
    body: JSON.stringify({
      email: String(formData.get("email") ?? "").trim(),
      first_name: String(formData.get("first_name") ?? "").trim() || undefined,
      consent: formData.get("consent") === "true",
      source: String(formData.get("source") ?? "footer"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true as const };
  return data(
    { ok: false as const, fields: (result.error.fields ?? {}) as Record<string, string[]>, message: String(result.error.message) },
    { status: response.status },
  );
}
```

`app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [index("routes/home.tsx"), route("newsletter", "routes/newsletter.tsx")] satisfies RouteConfig;
```

`app/components/newsletter-form.tsx`:

```tsx
import { useFetcher } from "react-router";
import type { action } from "~/routes/newsletter";

export function NewsletterForm() {
  const fetcher = useFetcher<typeof action>();
  const result = fetcher.data;
  const busy = fetcher.state !== "idle";

  if (result?.ok) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <fetcher.Form method="post" action="/newsletter">
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {result?.fields.email ? <p>{result.fields.email[0]}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {result?.fields.consent ? <p>{result.fields.consent[0]}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {result && !result.fields.email && !result.fields.consent ? <p role="alert">{result.message}</p> : null}
      <button disabled={busy}>{busy ? "Subscribing" : "Subscribe"}</button>
    </fetcher.Form>
  );
}
```

Pour aller plus loin avec React Router, lisez [Créer un formulaire d'inscription à une newsletter avec React Router](https://ikiform.com/fr/guides/newsletter-signup/react-router.md).

**SvelteKit**

Les actions vivent dans un `+page.server.ts`, jamais dans un layout : l'action est donc sur une page `/newsletter` et le pied de page envoie à `/newsletter?/subscribe`. Le callback `use:enhance` personnalisé traite lui-même le résultat au lieu d'appeler `update()`, pour que le lecteur reste sur l'article. Déclarez `IKIFORM_SECRET_KEY` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse la lire.

`src/routes/newsletter/+page.server.ts`:

```ts
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";

export const actions = {
  subscribe: async ({ request, fetch, url }) => {
    const form = await request.formData();
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify({
        email: String(form.get("email") ?? "").trim(),
        first_name: String(form.get("first_name") ?? "").trim() || undefined,
        consent: form.get("consent") === "true",
        source: String(form.get("source") ?? "footer"),
        _gotcha: String(form.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields = result.error.fields ?? {};
      return fail(response.status, { message: String(fields.email?.[0] ?? fields.consent?.[0] ?? result.error.message) });
    }
    return { subscribed: true };
  },
} satisfies Actions;
```

`src/lib/NewsletterForm.svelte`:

```svelte
<script lang="ts">
  import { applyAction, enhance } from "$app/forms";

  let done = $state(false);
  let message = $state("");
  let pending = $state(false);
</script>

{#if done}
  <p role="status">Almost done. Open the email we sent you and confirm your address.</p>
{:else}
  <form
    method="POST"
    action="/newsletter?/subscribe"
    use:enhance={() => {
      pending = true;
      return async ({ result }) => {
        pending = false;
        if (result.type === "success") done = true;
        else if (result.type === "failure") message = String(result.data?.message ?? "Couldn't subscribe.");
        else await applyAction(result);
      };
    }}
  >
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" value="true" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if message}
      <p role="alert">{message}</p>
    {/if}
    <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
  </form>
{/if}
```

Pour aller plus loin avec SvelteKit, lisez [Créer un formulaire d'inscription à une newsletter avec SvelteKit](https://ikiform.com/fr/guides/newsletter-signup/sveltekit.md).

**Nuxt**

Une route serveur garde la clé secrète hors du navigateur et renvoie les erreurs de champ d'Ikiform dans le `data` de l'erreur. Définissez `NUXT_IKIFORM_SECRET_KEY`, et déclarez `ikiformSecretKey` sous `runtimeConfig` dans `nuxt.config.ts`.

`server/api/newsletter.post.ts`:

```ts
interface Signup {
  email?: string;
  first_name?: string;
  consent?: boolean;
  source?: string;
  _gotcha?: string;
}

export default defineEventHandler(async (event) => {
  const body = await readBody<Signup>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.ikiformSecretKey}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify({
      email: body.email?.trim() ?? "",
      first_name: body.first_name?.trim() || undefined,
      consent: body.consent === true,
      source: body.source ?? "footer",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.message),
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { subscribed: true };
});
```

`app/components/NewsletterForm.vue`:

```vue
<script setup lang="ts">
const email = ref("");
const firstName = ref("");
const consent = ref(false);
const trap = ref("");
const status = ref<"idle" | "sending" | "done">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function subscribe() {
  status.value = "sending";
  errors.value = {};
  message.value = "";
  try {
    await $fetch("/api/newsletter", {
      method: "POST",
      body: { email: email.value, first_name: firstName.value, consent: consent.value, source: "footer", _gotcha: trap.value },
    });
    status.value = "done";
  } catch (failure) {
    const data = (failure as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    errors.value = data?.data?.fields ?? {};
    message.value = data?.message ?? "Couldn't subscribe. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'done'" role="status">Almost done. Open the email we sent you and confirm your address.</p>
  <form v-else @submit.prevent="subscribe">
    <label for="nl-email">Email</label>
    <input id="nl-email" v-model="email" name="email" type="email" required autocomplete="email" />
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" v-model="firstName" name="first_name" type="text" maxlength="80" />
    <label>
      <input v-model="consent" name="consent" type="checkbox" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <p v-if="errors.consent">{{ errors.consent[0] }}</p>
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="message && !errors.email && !errors.consent" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Subscribing" : "Subscribe" }}</button>
  </form>
</template>
```

Pour aller plus loin avec Nuxt, lisez [Créer un formulaire d'inscription à une newsletter avec Nuxt](https://ikiform.com/fr/guides/newsletter-signup/nuxt.md).

**React**

Une single-page app envoie depuis le navigateur. `submit()` de `@ikiform/sdk` envoie le `FormData` et se résout toujours avec `ok` et, en cas d'échec, les erreurs de champ : il n'y a donc pas de bloc `try` à écrire. Ne mettez jamais de clé secrète ici ; c'est **Allowed domains** (domaines autorisés) qui protège le formulaire.

`src/NewsletterForm.tsx`:

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, useState } from "react";

type FieldErrors = Record<string, string[]>;

export function NewsletterForm() {
  const [done, setDone] = useState(false);
  const [errors, setErrors] = useState<FieldErrors>({});
  const [message, setMessage] = useState("");
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    setErrors({});
    setMessage("");
    const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.currentTarget));
    setPending(false);
    if (result.ok) {
      setDone(true);
      return;
    }
    setErrors(result.error.fields ?? {});
    setMessage(result.error.message);
  }

  if (done) return <p role="status">Almost done. Open the email we sent you and confirm your address.</p>;

  return (
    <form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" onSubmit={onSubmit}>
      <label htmlFor="nl-email">Email</label>
      <input id="nl-email" name="email" type="email" required autoComplete="email" />
      {errors.email ? <p>{errors.email[0]}</p> : null}
      <label htmlFor="nl-name">First name (optional)</label>
      <input id="nl-name" name="first_name" type="text" maxLength={80} />
      <label>
        <input name="consent" type="checkbox" value="true" required />
        Email me the newsletter. I can unsubscribe at any time.
      </label>
      {errors.consent ? <p>{errors.consent[0]}</p> : null}
      <input type="hidden" name="source" value="footer" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {message && !errors.email && !errors.consent ? <p role="alert">{message}</p> : null}
      <button disabled={pending}>{pending ? "Subscribing" : "Subscribe"}</button>
    </form>
  );
}
```

**Vue**

Le même appel `submit()` dans un composant Vue monofichier. Le formulaire garde son `action` : il envoie donc quand même si le script ne se charge pas.

`src/components/NewsletterForm.vue`:

```vue
<script setup lang="ts">
import { submit } from "@ikiform/sdk";
import { ref } from "vue";

const status = ref<"idle" | "sending" | "done">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function subscribe(event: SubmitEvent) {
  status.value = "sending";
  const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.target as HTMLFormElement));
  if (result.ok) {
    status.value = "done";
    return;
  }
  errors.value = result.error.fields ?? {};
  message.value = result.error.message;
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'done'" role="status">Almost done. Open the email we sent you and confirm your address.</p>
  <form v-else action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post" @submit.prevent="subscribe">
    <label for="nl-email">Email</label>
    <input id="nl-email" name="email" type="email" required autocomplete="email" />
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label for="nl-name">First name (optional)</label>
    <input id="nl-name" name="first_name" type="text" maxlength="80" />
    <label>
      <input name="consent" type="checkbox" value="true" required />
      Email me the newsletter. I can unsubscribe at any time.
    </label>
    <p v-if="errors.consent">{{ errors.consent[0] }}</p>
    <input type="hidden" name="source" value="footer" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="message && !errors.email && !errors.consent" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Subscribing" : "Subscribe" }}</button>
  </form>
</template>
```

- Remplacez `frm_xxxxxxxxxxxxxxxx` par l'id de votre formulaire, visible dans le tableau de bord.
- Gardez `email` comme nom de l'input e-mail. Ikiform le valide, met le domaine en minuscules et le compare aux domaines d'e-mails jetables.
- L'input masqué `_gotcha` est le honeypot. Les humains ne le voient jamais, et les bots qui remplissent tous les inputs se signalent eux-mêmes comme spam.
- La case envoie `true` quand elle est cochée. Décochée, elle n'envoie rien, et une case obligatoire est refusée avec `This box must be checked`.
- La ligne de succès invite à vérifier la boîte de réception, car votre outil d'e-mailing envoie une confirmation. Si vous avez désactivé le double opt-in, remplacez-la par un simple merci.
- Les versions côté serveur transmettent l'en-tête `Origin` du navigateur, car c'est grâce à lui qu'Ikiform reconnaît les tests gratuits depuis `localhost`, et le `fetch` d'un serveur n'en envoie aucun.

> Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur.

La configuration des frameworks est dans la documentation : [Next.js](https://ikiform.com/fr/docs/guides/nextjs.md), [Astro](https://ikiform.com/fr/docs/guides/astro.md), [SvelteKit](https://ikiform.com/fr/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/fr/docs/guides/nuxt.md), [React](https://ikiform.com/fr/docs/guides/react.md), [Vue](https://ikiform.com/fr/docs/guides/vue.md) et [HTML simple](https://ikiform.com/fr/docs/html.md). Le [modèle de formulaire de newsletter](https://ikiform.com/forms/newsletter-signup) génère les mêmes champs en HTML, en React et en server action Next.js.

## Les champs et la définition

Collez la définition ci-dessous dans l'onglet **Fields** du formulaire et Ikiform l'applique côté serveur, quoi qu'envoie la page. L'aperçu exécute le même validateur dans votre navigateur : laissez la case décochée et envoyez pour voir l'erreur que recevrait un bot ou une page cassée.

Formulaire : Newsletter

- **E-mail** (`email`, email, obligatoire)
- **Prénom** (`first_name`, text)
- **Envoyez-moi la newsletter. Je peux me désinscrire à tout moment.** (`consent`, checkbox, obligatoire)
- **Emplacement de l'inscription** (`source`, text)

Dans votre page, `source` est un input masqué. Il n'est affiché ici en champ texte que pour que vous puissiez l'essayer.

| Champ | Pourquoi | À garder en tête |
| --- | --- | --- |
| `email` | La personne abonnée | Obligatoire. `type="email"` affiche le bon clavier sur mobile |
| `first_name` | Une salutation dans chaque numéro | Facultatif. La plupart des outils d'e-mailing ont un champ prénom auquel l'associer |
| `consent` | Une preuve que la personne a demandé à recevoir des e-mails | Case obligatoire. Stockée comme `true` |
| `source` | Quel formulaire du site a fonctionné : pied de page, article, popup | Masqué. Définissez une valeur différente par emplacement, et transmettez-la comme tag ou champ |

Une fois les champs définis, Ikiform refuse tout input qu'il ne connaît pas avec `unknown_field` : ajoutez donc un champ à la définition avant d'ajouter un input à la page. Les noms qui commencent par `_`, comme `_redirect`, sont des instructions pour Ikiform et ne comptent jamais comme des champs.

## Enregistrer le consentement

Une newsletter, ce sont des e-mails que quelqu'un a demandés. La case rend cette demande explicite, et la soumission stockée l'enregistre. Voici ce qu'Ikiform conserve pour chaque inscription acceptée :

`data.fields`:

```json
{
  "email": "ada@example.com",
  "first_name": "Ada",
  "consent": true,
  "source": "footer"
}
```

À côté des champs, chaque soumission a son horodatage `created_at`, l'origine et le referrer de la page, le pays et le user agent du navigateur. Ensemble, ils montrent quand et où la personne a coché la case. Deux habitudes rendent cet enregistrement utile par la suite :

1. **Rédigez le libellé comme une promesse.** « Envoyez-moi la newsletter. Je peux me désinscrire à tout moment. » dit ce que la personne reçoit. Si vous envoyez aussi des offres, dites-le dans le libellé.
2. **Notez la date quand la formulation change.** Un `true` stocké dit que la personne a accepté, pas quel texte. Tenez un court historique daté du libellé, pour pouvoir rapprocher chaque inscription des mots qu'elle a vus.

Ikiform conserve l'enregistrement ; il ne peut pas vous dire ce qu'exigent les règles là où vous et vos lecteurs vivez. Vérifiez-les avec quelqu'un qui les connaît.

## Les e-mails de confirmation viennent de votre outil d'e-mailing

Ikiform n'envoie pas d'e-mail à la personne qui remplit le formulaire. Les e-mails de notification vont uniquement à vous et à votre équipe : il n'y a donc ni réponse automatique, ni e-mail de confirmation, ni double opt-in. Envoyez-les depuis votre outil d'e-mailing, déclenché par un [webhook](https://ikiform.com/fr/docs/webhooks.md). Cette séparation est voulue. L'outil qui envoie votre newsletter suit déjà qui a confirmé, qui s'est désinscrit et quelles adresses rebondissent. Si un second système envoyait les confirmations, ces données finiraient par diverger.

Le relais ajoute donc le lecteur dans un état qui pousse l'outil d'e-mailing à envoyer sa propre confirmation, et l'outil le marque comme actif quand il clique. Chaque outil active cela différemment : un réglage du formulaire, une option du compte, ou un paramètre de la requête API. Le [guide du double opt-in](https://ikiform.com/fr/guides/newsletter-signup/double-opt-in.md) couvre les trois cas, et ce qui se passe quand un abonné connu s'inscrit à nouveau.

## Le relais

Un relais est une courte fonction qui reçoit le webhook d'Ikiform, vérifie sa signature et appelle l'API de votre outil d'e-mailing. Il lui faut une URL HTTPS publique et un secret de chaque côté. Celui-ci est un route handler Next.js ; le même code tourne dans tout runtime qui dispose de `fetch`.

`app/api/newsletter/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { addSubscriber } from "@/lib/email-tool";

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const fields = event.data.fields;
  if (fields.consent !== true) return new Response("No consent, skipped", { status: 200 });

  const answer = await addSubscriber({
    email: String(fields.email),
    firstName: typeof fields.first_name === "string" ? fields.first_name : undefined,
    source: typeof fields.source === "string" ? fields.source : "unknown",
    referrer: typeof event.data.meta.referrer === "string" ? event.data.meta.referrer : undefined,
  });
  if (answer.status === 429) {
    return new Response("Email tool rate limit", {
      status: 503,
      headers: { "retry-after": answer.headers.get("retry-after") ?? "60" },
    });
  }
  if (!answer.ok) return new Response(`Email tool answered ${answer.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` vérifie la signature HMAC et l'horodatage. Sans lui, quiconque trouve l'URL peut ajouter des adresses à votre liste.
- Les événements `endpoint.test`, envoyés par **Send test** dans le tableau de bord, reçoivent `204` et sont ignorés.
- La vérification du consentement est un second verrou derrière la définition du formulaire.
- `addSubscriber` est la seule partie qui dépend de l'outil. Son contenu pour chaque outil est sur les pages [Kit](https://ikiform.com/fr/guides/newsletter-signup/kit.md), [MailerLite](https://ikiform.com/fr/guides/newsletter-signup/mailerlite.md) et [beehiiv](https://ikiform.com/fr/guides/newsletter-signup/beehiiv.md).

Ce que répond le relais décide de ce que fait Ikiform ensuite :

Schéma : Ce que le relais répond à Ikiform pour chaque réponse de l'outil d'e-mailing

1. L'outil d'e-mailing ajoute ou met à jour l'abonné : le relais répond 204 et la livraison est marquée comme réussie.
2. L'outil d'e-mailing limite le débit : le relais répond 503 avec Retry-After, et Ikiform attend ce délai avant la tentative suivante.
3. L'outil d'e-mailing refuse la requête ou est en panne : le relais répond 502, et Ikiform réessaie selon son calendrier avec le même webhook-id.

Chaque tentative figure dans le journal de livraison de l'endpoint avec la réponse de votre relais : un 502 qui indique le statut de l'outil vous dit quoi corriger.

Ajoutez l'URL du relais sous **Webhooks** dans le tableau de bord, choisissez le formulaire de newsletter et copiez le secret `whsec_` dans `IKIFORM_WEBHOOK_SECRET`. Un espace de travail a jusqu'à 3 endpoints. L'URL doit être en HTTPS public sur le port 443 ; les noms d'hôte `workers.dev` et `pages.dev` sont refusés, donnez donc un domaine personnalisé à un Worker. Les nouvelles tentatives, les payloads et le schéma de signature sont dans la [documentation des webhooks](https://ikiform.com/fr/docs/webhooks.md), et [les soumissions de formulaire vers votre propre serveur](https://ikiform.com/blog/form-submissions-to-your-own-server.md) propose une version qui stocke chaque livraison une seule fois.

> Pas de code ? Faites pointer le webhook vers un outil d'automatisation et utilisez son action pour votre outil d'e-mailing au lieu d'un relais. Voir [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md), [Make](https://ikiform.com/blog/form-submissions-to-make.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Il n'y a pas d'intégrations en un clic. Chaque outil est atteint par un [webhook](https://ikiform.com/fr/docs/webhooks.md) signé, directement ou via un outil d'automatisation comme [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).

## Tenir à l'écart les bots et le list bombing

Les inscriptions de bots nuisent davantage à une newsletter qu'à la plupart des formulaires. Une vraie adresse inscrite par un bot reçoit des e-mails qu'elle n'a jamais demandés, et les plaintes de ces personnes nuisent au domaine depuis lequel vous envoyez. Certains bots inscrivent l'adresse d'une victime à des centaines de formulaires d'un coup pour noyer sa boîte de réception, et chaque formulaire qui envoie un e-mail de confirmation y participe.

| Défense | Ce qu'elle arrête | Configuration |
| --- | --- | --- |
| Honeypot | Les bots qui remplissent tous les inputs | L'input masqué `_gotcha` |
| Consentement obligatoire | Les scripts qui n'envoient qu'un e-mail | La case `consent` avec `required: true` dans la définition |
| Score de spam | Adresses jetables (+2), répétitions identiques (+3), liens et phrases de spam. À 5, une inscription est stockée comme spam | Aucun |
| Limites de débit | Un visiteur qui envoie en boucle | Activée par défaut : Standard, 5 par minute et par visiteur |
| Vérification humaine | Les bots qui pilotent un vrai navigateur | Réglez **Spam check** sur **Ikiform** pour Cloudflare Turnstile, sans clés à gérer |
| Domaines autorisés | L'endpoint de votre formulaire copié sur d'autres sites | Listez chaque domaine où apparaît le formulaire |

Le spam reçoit la même réponse de succès qu'une vraie inscription et n'envoie aucun webhook : il n'atteint donc jamais votre outil d'e-mailing et ne déclenche jamais d'e-mail de confirmation. Si un vrai lecteur tombe en spam, marquez-le comme non-spam et son webhook part à ce moment-là. La [documentation anti-spam](https://ikiform.com/fr/docs/spam.md) explique chaque vérification et son score.

> Une limite stricte par visiteur convient à un formulaire de newsletter, car une personne s'abonne une seule fois. Si votre public lit depuis des bureaux ou des campus qui partagent une même IP, Standard les compte ensemble ; Relaxed, 20 par minute par visiteur, est alors le choix le plus sûr. Voir les [limites de débit](https://ikiform.com/fr/docs/rate-limits.md).

## Un formulaire sur de nombreuses pages

Un même endpoint peut se trouver dans le pied de page de chaque page, à la fin de chaque article et sur une page `/newsletter`. Donnez à chaque emplacement sa propre valeur `source` pour savoir lequel rapporte des inscriptions :

- `footer` : vu sur chaque page. Limitez-le à l'e-mail et à la case.
- `post` : des lecteurs qui ont fini un article, souvent ceux qui convertissent le mieux.
- `newsletter-page` : là où pointent vos profils sociaux. De la place pour un numéro d'exemple.

Chaque soumission enregistre aussi le referrer, qui est la page exacte où se trouvait le lecteur : `source` vous donne l'emplacement et le referrer l'article. Ajoutez chaque domaine qui affiche le formulaire sous **Allowed domains**, jusqu'à 20 par formulaire ; les sous-domaines d'un domaine listé sont autorisés automatiquement.

### Le même lecteur deux fois

Ikiform ne fusionne ni ne rejette les soumissions répétées par e-mail. Une répétition identique dans les 24 heures ne fait qu'augmenter le score de spam. Dédupliquez là où vit la liste, ou à l'export. Pour une newsletter, c'est rarement un problème : Kit, MailerLite et beehiiv identifient chacun les abonnés par e-mail, donc une répétition met à jour un seul contact au lieu d'en créer un second.

## Changer d'outil d'e-mailing plus tard

C'est là que posséder le formulaire est rentable. Passer d'un outil d'e-mailing à un autre implique normalement de remplacer chaque embed du site. Ici, le formulaire ne sait pas quel outil se trouve derrière lui :

Schéma : Ce qui change quand vous passez à un autre outil d'e-mailing

1. Avec un embed, chaque page porte du code qui envoie directement à l'ancien outil : migrer, c'est remplacer chaque copie.
2. Avec votre propre formulaire, chaque page continue d'envoyer à Ikiform, et Ikiform continue d'envoyer le même webhook.
3. Seule la fonction addSubscriber du relais change, pour appeler l'API du nouvel outil.

Faites tourner les deux outils pendant une semaine si vous voulez : deux endpoints de webhook, deux relais, un formulaire. Supprimez ensuite l'ancien endpoint.

Migrez les abonnés existants avec l'export de l'ancien outil, car lui seul sait qui a confirmé et qui est parti. L'export CSV d'Ikiform, jusqu'à 100 000 lignes par fichier, sert à vérifier les chiffres, pas à importer.

## Choisissez votre cas

Le formulaire ci-dessus est le même partout. Ces pages couvrent les e-mails de confirmation, les sites sans serveur et les appels API exacts pour trois outils d'e-mailing.

- [Double opt-in](https://ikiform.com/fr/guides/newsletter-signup/double-opt-in.md): Ikiform n'envoie pas d'e-mails de confirmation : le double opt-in se fait donc dans votre outil d'e-mailing. Un relais ajoute chaque inscription comme non confirmée et l'outil envoie le lien.
- [Site statique](https://ikiform.com/fr/guides/newsletter-signup/static-site.md): Ajoutez un formulaire d'inscription à la newsletter à un site statique Hugo, Jekyll, Eleventy ou Astro, gardez la clé d'API de l'outil d'e-mailing hors de la page et confirmez dans l'outil.
- [Kit (ConvertKit)](https://ikiform.com/fr/guides/newsletter-signup/kit.md): Reliez votre propre formulaire de newsletter à Kit avec un relais qui crée l'abonné, l'ajoute à un formulaire Kit pour l'incentive email et étiquette l'emplacement.
- [MailerLite](https://ikiform.com/fr/guides/newsletter-signup/mailerlite.md): Reliez votre propre formulaire d'inscription à MailerLite : un relais ajoute les nouveaux lecteurs comme non confirmés pour le double opt-in, sans jamais demander deux fois à un abonné de confirmer.
- [beehiiv](https://ikiform.com/fr/guides/newsletter-signup/beehiiv.md): Reliez votre propre formulaire d'inscription à beehiiv : un relais crée chaque abonnement avec le double opt-in activé, l'emplacement comme source UTM et la page comme referrer.
- [Formulaire de newsletter Next.js](https://ikiform.com/fr/guides/newsletter-signup/nextjs.md): Un formulaire de newsletter en pied de page Next.js : une server action avec vérification du consentement et Turnstile, qui envoie à Ikiform, plus un route handler qui ajoute les lecteurs à Kit.
- [Formulaire de newsletter Astro](https://ikiform.com/fr/guides/newsletter-signup/astro.md): Un formulaire de newsletter Astro qui reste sur des pages prérendues : une Astro Action vérifie le consentement et envoie à Ikiform, et un endpoint ajoute les lecteurs à MailerLite.
- [Formulaire de newsletter TanStack Start](https://ikiform.com/fr/guides/newsletter-signup/tanstack-start.md): Un formulaire de newsletter TanStack Start : une server function pour les inscriptions dans la page, une server route en repli sans JavaScript, et les vraies inscriptions envoyées à beehiiv.
- [Formulaire de newsletter React Router](https://ikiform.com/fr/guides/newsletter-signup/react-router.md): Un formulaire de newsletter en pied de page avec React Router : useFetcher envoie à une route /newsletter qui fonctionne aussi sans JavaScript, et une resource route alimente MailerLite.
- [Formulaire de newsletter SvelteKit](https://ikiform.com/fr/guides/newsletter-signup/sveltekit.md): Un formulaire de newsletter SvelteKit 3 dans le layout : une form action avec consentement et limite par IP, un use:enhance qui reste sur la page, et une route de webhook pour Kit.
- [Formulaire de newsletter Nuxt](https://ikiform.com/fr/guides/newsletter-signup/nuxt.md): Un formulaire de newsletter Nuxt qui fonctionne avec ou sans JavaScript : une route serveur avec vérification Turnstile envoie à Ikiform, et une route Nitro alimente beehiiv.

## Vérifiez que tout fonctionne

Faites ces vérifications avant d'ajouter le formulaire à chaque page. Ikiform n'a pas d'offre gratuite. Les tests sont gratuits : un espace de travail sans offre accepte 50 soumissions de test par mois depuis `localhost`. Pour passer en production, choisissez Pro à partir de 6 $/mois pour 500 soumissions, ou payez une seule fois : 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais. Voir les [tarifs](https://ikiform.com/fr/pricing).

1. Dans **Allowed domains** du formulaire, ajoutez le domaine de votre site, plus `localhost` pendant le développement. Laissez vide pour accepter les envois de partout.
2. Envoyez le formulaire avec la case décochée depuis les outils de développement du navigateur, ou retirez `required` un instant. Vous obtenez `422` avec `This box must be checked` et rien n'est stocké.
3. Envoyez-le correctement avec une adresse à vous. Vous voyez la ligne vous invitant à vérifier votre boîte, ou vous arrivez sur votre page de remerciement.
4. Ouvrez **Submissions** dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
5. Ouvrez l'endpoint sous **Webhooks**. La livraison affiche `succeeded` et la réponse de votre relais.
6. Ouvrez votre outil d'e-mailing. L'abonné y est, non confirmé si le double opt-in est activé, et l'e-mail de confirmation est dans votre boîte.
7. Envoyez avec le honeypot rempli, `_gotcha=bot`. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.
8. Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur **Spam**. Le spam reçoit la même réponse de succès qu'une vraie soumission.

## Dépannage

| Problème | Que faire |
| --- | --- |
| `422 validation_failed` avec `consent` | La case n'a pas été cochée, ou le `name` de son input n'est pas `consent`. Une version côté serveur doit envoyer `true`, `"true"` ou `"on"` ; une case décochée n'envoie rien. |
| `422 validation_failed` avec `unknown_field` | La page envoie un input que la définition ne liste pas, souvent un `source` masqué ou un input de suivi ajouté plus tard. Ajoutez-le à la définition, ou supprimez l'input. |
| `403 origin_not_allowed`, ou tous les envois finissent en spam | Le domaine de la page n'est pas dans les **Allowed domains** du formulaire. Ajoutez-le, plus `localhost` pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés. |
| `429 rate_limited` | Un visiteur a envoyé plus que ce que permet la limite de débit du formulaire. Standard autorise 5 envois par minute et par visiteur. Attendez la durée de `Retry-After`, ou choisissez Relaxed ou Custom pour les formulaires qui envoient depuis une seule IP partagée. |
| La soumission est dans le tableau de bord mais aucun e-mail ni webhook n'est arrivé | Elle a été stockée comme spam. Le spam n'envoie rien. Ouvrez-la sous le filtre **Spam** pour voir les raisons, et marquez-la comme non-spam pour envoyer son e-mail et ses webhooks. |
| Les inscriptions arrivent dans Ikiform mais pas dans l'outil d'e-mailing | Lisez le journal de livraison sous **Webhooks**. `400` signifie que le secret du relais est faux. `502` porte le statut de l'outil d'e-mailing : généralement une clé d'API, un id de liste ou un id de formulaire erroné. |
| L'abonné est dans l'outil d'e-mailing mais n'a reçu aucun e-mail de confirmation | Le double opt-in est désactivé dans l'outil, ou le relais a créé l'abonné comme déjà confirmé. Voir [double opt-in](https://ikiform.com/fr/guides/newsletter-signup/double-opt-in.md) pour le réglage de chaque outil. |
| Le navigateur arrive sur la page de remerciement d'Ikiform au lieu de la vôtre | `_redirect` est ignoré sauf si son domaine figure dans **Allowed domains** et qu'il s'agit d'une URL `https:`. Ajoutez le domaine, ou définissez plutôt **Redirect after submit** dans les réglages du formulaire. |

## Questions

### Puis-je créer un formulaire d'inscription à la newsletter sans le code d'embed de Mailchimp ?

Oui. Écrivez le formulaire dans votre propre HTML, envoyez-le à un backend de formulaires comme Ikiform, et transmettez chaque inscription par webhook à Mailchimp ou à tout autre outil d'e-mailing via son API. Le formulaire ressemble alors au reste de votre site et ne change pas quand l'outil d'e-mailing change.

### Ikiform envoie-t-il l'e-mail de confirmation ou la newsletter ?

Non. Ikiform recueille, valide et stocke les inscriptions, puis les transmet par webhook. Il n'écrit jamais à l'abonné : l'e-mail de confirmation, chaque numéro et le lien de désinscription viennent de votre outil d'e-mailing.

### Avec quels outils d'e-mailing cela fonctionne-t-il ?

Tout outil qui a une API pour ajouter des abonnés, ou tout outil pris en charge par un service d'automatisation comme Zapier, Make ou n8n. Il n'y a pas d'intégrations en un clic ; c'est le relais ou l'automatisation qui fait le lien. Kit, MailerLite et beehiiv ont des pages pas à pas dans ce guide.

### Où garder la clé d'API de mon outil d'e-mailing ?

Dans les variables d'environnement côté serveur du relais, jamais dans la page. La clé d'API d'un outil d'e-mailing peut généralement lire et modifier toute la liste : une clé dans du JavaScript du navigateur, c'est une clé que n'importe qui peut copier.

### Comment empêcher les bots de s'inscrire à ma newsletter ?

Gardez le honeypot masqué _gotcha et la case de consentement obligatoire, listez vos domaines sous Allowed domains et réglez la vérification anti-spam sur Ikiform pour une vérification Cloudflare Turnstile. Les inscriptions stockées comme spam n'envoient jamais de webhook, elles ne déclenchent donc jamais d'e-mail de confirmation.

### Ikiform supprime-t-il les soumissions en double ?

Non. Chaque envoi accepté est stocké. Une répétition identique dans les 24 heures augmente le score de spam mais n'est pas du spam en soi : dédoublonnez donc par e-mail dans votre liste, votre base de données ou votre export CSV.

### Ikiform est-il gratuit ?

Non, il n'y a pas d'offre gratuite. Les tests sont gratuits : vous pouvez envoyer 50 soumissions de test par mois depuis localhost avant de payer. Pour passer en production, Pro commence à 6 $/mois pour 500 soumissions, ou vous pouvez payer une seule fois 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais.

## Autres guides

- [Double opt-in](https://ikiform.com/fr/guides/newsletter-signup/double-opt-in.md): Ikiform n'envoie pas d'e-mails de confirmation : le double opt-in se fait donc dans votre outil d'e-mailing. Un relais ajoute chaque inscription comme non confirmée et l'outil envoie le lien.
- [Site statique](https://ikiform.com/fr/guides/newsletter-signup/static-site.md): Ajoutez un formulaire d'inscription à la newsletter à un site statique Hugo, Jekyll, Eleventy ou Astro, gardez la clé d'API de l'outil d'e-mailing hors de la page et confirmez dans l'outil.
- [Kit (ConvertKit)](https://ikiform.com/fr/guides/newsletter-signup/kit.md): Reliez votre propre formulaire de newsletter à Kit avec un relais qui crée l'abonné, l'ajoute à un formulaire Kit pour l'incentive email et étiquette l'emplacement.
- [MailerLite](https://ikiform.com/fr/guides/newsletter-signup/mailerlite.md): Reliez votre propre formulaire d'inscription à MailerLite : un relais ajoute les nouveaux lecteurs comme non confirmés pour le double opt-in, sans jamais demander deux fois à un abonné de confirmer.
- [beehiiv](https://ikiform.com/fr/guides/newsletter-signup/beehiiv.md): Reliez votre propre formulaire d'inscription à beehiiv : un relais crée chaque abonnement avec le double opt-in activé, l'emplacement comme source UTM et la page comme referrer.
- [Formulaire de newsletter Next.js](https://ikiform.com/fr/guides/newsletter-signup/nextjs.md): Un formulaire de newsletter en pied de page Next.js : une server action avec vérification du consentement et Turnstile, qui envoie à Ikiform, plus un route handler qui ajoute les lecteurs à Kit.
- [Formulaire de newsletter Astro](https://ikiform.com/fr/guides/newsletter-signup/astro.md): Un formulaire de newsletter Astro qui reste sur des pages prérendues : une Astro Action vérifie le consentement et envoie à Ikiform, et un endpoint ajoute les lecteurs à MailerLite.
- [Formulaire de newsletter TanStack Start](https://ikiform.com/fr/guides/newsletter-signup/tanstack-start.md): Un formulaire de newsletter TanStack Start : une server function pour les inscriptions dans la page, une server route en repli sans JavaScript, et les vraies inscriptions envoyées à beehiiv.
- [Formulaire de newsletter React Router](https://ikiform.com/fr/guides/newsletter-signup/react-router.md): Un formulaire de newsletter en pied de page avec React Router : useFetcher envoie à une route /newsletter qui fonctionne aussi sans JavaScript, et une resource route alimente MailerLite.
- [Formulaire de newsletter SvelteKit](https://ikiform.com/fr/guides/newsletter-signup/sveltekit.md): Un formulaire de newsletter SvelteKit 3 dans le layout : une form action avec consentement et limite par IP, un use:enhance qui reste sur la page, et une route de webhook pour Kit.
- [Formulaire de newsletter Nuxt](https://ikiform.com/fr/guides/newsletter-signup/nuxt.md): Un formulaire de newsletter Nuxt qui fonctionne avec ou sans JavaScript : une route serveur avec vérification Turnstile envoie à Ikiform, et une route Nitro alimente beehiiv.
- [Envoi de formulaire HTML](https://ikiform.com/fr/guides/html-form-submission.md): Ce que fait un navigateur quand un formulaire est envoyé : quels champs il transmet, comment il les encode, ce que le serveur doit répondre, et comment Ikiform répond.
- [Inscription à un événement](https://ikiform.com/fr/guides/event-registration.md): Recevez les RSVP sur votre page d'événement : formulaire, limite d'invités, décompte fiable, clôture quand c'est complet et liste d'accueil, sans billetterie.
- [Vérifier les signatures de webhook](https://ikiform.com/fr/guides/webhook-signature-verification.md): Vérifiez qu'un webhook de formulaire vient bien d'Ikiform : le schéma HMAC-SHA256 de Standard Webhooks, un vecteur de test, les corps bruts, les timestamps et la rotation du secret.
