Dépasser les formulaires intégrés

Dépasser les formulaires intégrés8 min de lecture

Formulaires Framer : quand ajouter un backend de formulaires, et comment

Les formulaires Framer envoient déjà vers l'e-mail, Sheets ou un webhook ; ajoutez Ikiform comme webhook pour des soumissions stockées et filtrées, des alertes d'équipe et davantage de destinations.

Preet SutharFondateur, Ikiform

Les formulaires natifs de Framer envoient chaque soumission par e-mail, vers Google Sheets ou vers un webhook, avec une protection anti-spam intégrée, donc pour une seule boîte de réception ou une seule feuille vous n'avez peut-être besoin de rien d'autre. Ajoutez Ikiform comme destination webhook quand vous voulez que chaque soumission soit stockée avec un statut de spam, validée selon vos champs, envoyée par e-mail à l'équipe et transmise à plusieurs webhooks signés. Comme Framer envoie depuis ses serveurs, assouplissez les vérifications d'origine du formulaire ou placez entre les deux un petit relais qui vérifie la signature de Framer.

Ce qui change sur Framer

Framer est le seul créateur de sites de ce guide sans limite à contourner. Ses formulaires peuvent envoyer par e-mail, vers Google Sheets ou vers n'importe quel webhook, et Framer a une protection anti-spam intégrée qui signale ou bloque les soumissions suspectes. Ses webhooks sont signés : chaque requête porte un en-tête Framer-Signature, un HMAC-SHA256 du corps et de l'identifiant de soumission, et un Framer-Webhook-Submission-Id. Framer réessaie jusqu'à cinq fois quand votre endpoint ne répond pas 2xx, et ne suit pas les redirections.

Soyez donc honnête sur la raison d'ajouter un backend de formulaires. Voici ce que couvre chaque côté :

Vous voulezFramer seulAvec Ikiform comme webhook
Soumissions dans une seule boîte ou une seule feuilleOuiOui, plus le tableau de bord
Un historique consultable avec le spam signalé, et un export CSVVotre boîte de réception ou votre feuille fait office d'historiqueOui
Vérifications côté serveur des types, longueurs et optionsNonOui, avec une définition de champs
Alertes à plusieurs personnes, avec Répondre qui va à l'auteur de la soumissionDestination e-mailJusqu'à 10 destinataires, avec adresse de réponse
Plusieurs outils alimentés par un seul formulaire, chacun avec nouvelles tentativesUn webhook par destination ajoutéeJusqu'à 3 endpoints signés, avec nouvelles tentatives et journal de livraison
Le même backend pour les sites Framer, Webflow et codésNonOui

Si la première ligne suffit à vos besoins, restez sur les destinations de Framer. La suite de cette page est pour les cas où elle ne suffit pas.

Le piège : Framer envoie depuis ses serveurs

Une destination webhook signifie que ce sont les serveurs de Framer qui envoient, pas le navigateur du visiteur. Trois choses changent côté Ikiform :

  • Pas d'origine. Les envois serveur ne portent pas d'en-tête Origin, donc avec Allowed domains défini, chaque envoi est traité comme venant du mauvais site.
  • Adresses IP partagées. Ikiform compte les limites de débit par adresse IP, et chaque envoi vient de quelques adresses Framer au lieu de celles de vos visiteurs.
  • Pas de honeypot. Framer envoie les inputs que vous avez construits et rien d'autre, et son formulaire ne peut pas exécuter la vérification humaine d'Ikiform.

Vous pouvez l'accepter, ou le corriger avec un relais. Les deux chemins sont décrits ci-dessous.

Un formulaire Framer qui envoie à Ikiform directement, et via un relais signéWebhook directRelais signéformulaireFramerserveursFramerJSON, sansorigineIkiformdomainesvides, limiteRelaxedformulaireFramerserveursFramersigne chaqueenvoivotreWorkervérifieFramer-SignatureIkiformclé secrète,cléd'idempotence401,rejetéenvoiwebhookenvoiwebhooktransmettresignature invalideUn formulaire Framer qui envoie à Ikiform directement, et via un relais signéWebhook directRelais signéformulaireFramerserveurs FramerJSON, sans origineIkiformdomaines vides,limite RelaxedformulaireFramerserveurs Framersigne chaque envoivotre WorkervérifieFramer-SignatureIkiformclé secrète, cléd'idempotence401,rejetéenvoiwebhookenvoiwebhooktransmettresignature invalide
  1. Direct : un visiteur envoie le formulaire Framer, les serveurs de Framer le transmettent en JSON sans en-tête Origin, et Ikiform l'accepte avec Allowed domains vide et une limite de débit Relaxed.
  2. Relais : les serveurs de Framer signent chaque envoi avec votre secret et l'envoient à un Worker que vous déployez.
  3. Le Worker recalcule la signature de Framer. Un envoi sans signature valide reçoit un 401 et ne va pas plus loin.
  4. Un envoi valide est transmis à Ikiform avec votre clé secrète et l'identifiant de soumission de Framer comme clé d'idempotence, donc une nouvelle tentative de Framer ne stocke jamais de seconde copie.
Le relais tient en une douzaine de lignes de vraie logique. En échange, le formulaire peut garder des domaines autorisés stricts, car seul votre Worker détient la clé secrète.

Option 1 : Ikiform comme webhook

  1. Dans Framer, donnez à chaque input un nom en minuscules : name, work_email, company, team_size. Framer utilise les noms comme clés JSON.
  2. Sélectionnez le formulaire et, à côté de Send To, choisissez Add… > Webhook. Collez votre endpoint. Laissez le secret vide ; Ikiform ne vérifie pas Framer-Signature.
  3. Dans les paramètres du formulaire Ikiform, laissez Allowed domains vide, laissez Spam check désactivé, et réglez Rate limit sur Relaxed.
  4. Publiez. Framer n'envoie de webhooks que depuis le site publié.
URL du webhook
https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx

Ikiform répond 201 une fois la soumission stockée, ce que Framer compte comme livré. Un 422 validation_failed échoue de la même façon aux cinq nouvelles tentatives, donc corrigez les noms des inputs plutôt que d'attendre. Le score de spam s'applique quand même à chaque envoi : liens, expressions de spam, adresses jetables et répétitions.

Option 2 : un relais signé

Déployez un Cloudflare Worker qui vérifie la signature de Framer et transmet les envois valides avec votre clé secrète. Le formulaire Ikiform peut alors lister votre domaine Framer sous Allowed domains avec Reject, pour que tout ce qui n'est ni un navigateur sur votre site ni votre Worker soit refusé.

src/index.ts
interface Env {
  FRAMER_SECRET: string;
  IKIFORM_SECRET_KEY: string;
}

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const encoder = new TextEncoder();

async function signatureFor(secret: string, body: ArrayBuffer, submissionId: string) {
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const id = encoder.encode(submissionId);
  const message = new Uint8Array(body.byteLength + id.byteLength);
  message.set(new Uint8Array(body), 0);
  message.set(id, body.byteLength);
  const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, message));
  return `sha256=${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}

function sameText(a: string, b: string) {
  const left = encoder.encode(a);
  const right = encoder.encode(b);
  return left.byteLength === right.byteLength && crypto.subtle.timingSafeEqual(left, right);
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") return new Response(null, { status: 405 });
    const signature = request.headers.get("framer-signature") ?? "";
    const submissionId = request.headers.get("framer-webhook-submission-id") ?? "";
    const body = await request.arrayBuffer();
    if (!submissionId || !sameText(signature, await signatureFor(env.FRAMER_SECRET, body, submissionId))) {
      return new Response("Bad signature", { status: 401 });
    }

    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${env.IKIFORM_SECRET_KEY}`,
        "Idempotency-Key": submissionId,
      },
      body,
    });
    if (response.ok) return new Response(null, { status: 204 });
    return new Response(await response.text(), { status: response.status });
  },
};

Le déployer

Terminal
npx wrangler secret put FRAMER_SECRET
npx wrangler secret put IKIFORM_SECRET_KEY
npx wrangler deploy
  • FRAMER_SECRET est le secret que vous définissez sur la destination webhook de Framer. Framer exige au moins 32 caractères et ne le réaffichera pas, donc générez-le, conservez-le et collez-le aux deux endroits.
  • IKIFORM_SECRET_KEY est une clé API secrète du tableau de bord Ikiform. Ne la placez jamais dans Framer.
  • La signature est sha256= suivi du HMAC hexadécimal du corps brut suivi de l'identifiant de soumission. Lisez le corps une seule fois en octets et utilisez ces mêmes octets pour la vérification et la transmission.
  • Idempotency-Key fait qu'une nouvelle tentative de Framer rejoue la première réponse d'Ikiform au lieu de stocker un doublon. Les clés sont conservées 24 heures.
  • Le Worker transmet le statut d'Ikiform tel quel. Framer réessaie toute réponse autre que 2xx, jusqu'à cinq fois, donc un 429 ou un 5xx a une autre chance, alors qu'un 422 échoue de la même façon à chaque fois jusqu'à ce que vous corrigiez les noms des inputs.

Faites ensuite pointer le webhook de Framer vers l'URL du Worker au lieu de l'endpoint, avec le secret renseigné.

Chaque envoi vient désormais de votre Worker. 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 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.

Option 3 : se passer du formulaire Framer

Un code component affiche un vrai formulaire et envoie depuis le navigateur du visiteur, donc les vérifications d'origine, le honeypot et les limites de débit par visiteur fonctionnent comme sur n'importe quel site. Vous perdez l'édition visuelle de Framer pour ce formulaire. Le guide Framer de la documentation contient le composant.

Vérifier que tout fonctionne sur Framer

  1. Publiez le site Framer et envoyez le formulaire depuis le site, pas depuis l'aperçu de l'éditeur.
  2. Ouvrez Submissions dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
  3. Vérifiez que les noms de champs correspondent exactement aux noms de vos inputs, sans clés ajoutées par Framer.
  4. Pour le relais : envoyez au Worker une requête avec un Framer-Signature inventé et vérifiez qu'il répond 401.
  5. Pour le relais : dans les détails de la soumission, l'origine est vide et le statut est reçu, pas spam.
  6. Vérifiez la boîte de réception de chaque destinataire des notifications. Chacun reçoit un e-mail avec les champs, et Reply répond à l'expéditeur quand le formulaire a un champ email.
  7. Si vous avez ajouté un webhook, ouvrez l'endpoint dans Webhooks. La livraison affiche succeeded avec la réponse de votre serveur.

Pièges

ProblèmeQue faire
Chaque envoi de webhook est classé en spam avec origin_mismatchAllowed domains est défini alors que Framer envoie directement. Videz-le, ou placez le relais entre les deux.
Le Worker répond 401 aux vrais envoisLe secret diffère entre Framer et le Worker, ou quelque chose a lu le corps comme texte et l'a modifié. Collez à nouveau le secret et hachez les octets bruts.
429 rate_limited pendant un lancementTous les envois partagent les adresses de Framer ou de Cloudflare. Choisissez Relaxed, ou une limite Custom.
422 validation_failedL'envoi ne correspond pas à la définition des champs du formulaire. error.fields nomme chaque champ et la raison. Faites correspondre le name de l'input à l'id du champ.
403 challenge_requiredLe spam check du formulaire est réglé sur Ikiform et un envoi fetch n'avait pas de jeton. Obtenez-en un avec challenge(formId) de @ikiform/sdk avant chaque envoi, ou désactivez la vérification.

Questions

Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.