Liste d'attente pour un nouveau SaaS

Liste d'attente pour un nouveau SaaS7 min de lecture

Comment ajouter une liste d'attente à un site Framer

Deux façons de collecter des inscriptions sur Framer : le formulaire Framer avec un webhook vers Ikiform, ou un code component qui garde limites et anti-spam.

Preet SutharFondateur, Ikiform

Les formulaires Framer ne peuvent pas envoyer vers une URL externe, mais ils peuvent envoyer chaque soumission à un webhook : la voie sans code est donc le formulaire Framer avec votre endpoint Ikiform comme webhook. Pour un lancement public, utilisez plutôt un petit code component : le navigateur envoie directement, donc la limite de débit par visiteur, les domaines autorisés et le honeypot d'Ikiform continuent tous de fonctionner.

Framer est l'outil de nombreuses pages d'avant-lancement, car la page elle-même se fait en un après-midi. C'est le formulaire de liste d'attente qui est moins évident. Le bloc formulaire de Framer ne peut pas changer son action : vous choisissez donc entre deux voies qui se comportent différemment une fois le vrai trafic arrivé.

Ce qui change sur Framer

  • Pas d'action personnalisée. Un formulaire Framer envoie vers les destinations de Framer : son propre stockage, l'e-mail, ou un Webhook. Un webhook est le seul moyen d'atteindre un backend externe sans code.
  • Les webhooks viennent des serveurs de Framer, pas du visiteur. Ikiform voit les adresses IP de Framer et aucun en-tête Origin. Cela désactive trois protections : les domaines autorisés, la limite de débit par visiteur et la vérification humaine.
  • Nouvelles tentatives. Framer traite tout ce qui n'est pas un 2xx comme un échec et réessaie jusqu'à cinq fois. Il ne suit pas les redirections. Ikiform répond à un envoi JSON par 201 : une inscription stockée est donc un succès dès la première fois.
  • Les code components sont du vrai React. Ils s'exécutent dans le navigateur du visiteur sur le site publié : un formulaire qu'ils contiennent envoie donc comme n'importe quelle page web.
Deux façons dont une inscription Framer arrive à IkiformFormulaire Framer avec un webhookCode componentformulaireFramersur votre pageserveurs FramerJSON, jusqu'à 5tentativesIkiformvoit l'IP de Framer,sans originecode componentformulaire React survotre pagenavigateur duvisiteurfetch avec cléd'idempotenceIkiformvoit le visiteur etvotre domaineenvoiwebhookenvoienvoiDeux façons dont une inscription Framer arrive à IkiformFormulaire Framer avec un webhookCode componentformulaire Framersur votre pageserveurs FramerJSON, jusqu'à 5 tentativesIkiformvoit l'IP de Framer, sansoriginecode componentformulaire React sur votre pagenavigateur du visiteurfetch avec clé d'idempotenceIkiformvoit le visiteur et votredomaineenvoiwebhookenvoienvoi
  1. Avec la voie webhook, le visiteur envoie le formulaire Framer et les serveurs de Framer transmettent les champs à Ikiform en JSON.
  2. Ikiform voit les serveurs de Framer comme expéditeur : il ne peut donc ni distinguer les visiteurs ni vérifier sur quel site se trouve le formulaire.
  3. Avec le code component, le navigateur du visiteur envoie directement à Ikiform.
  4. Ikiform voit l'IP de chaque visiteur et votre domaine : toutes les protections s'appliquent.
Les deux voies stockent la même inscription. Elles diffèrent par l'expéditeur que voit Ikiform.

Choisir une voie

ItemFormulaire Framer avec un webhookCode component
ConfigurationDeux champs dans la barre latérale droiteUn fichier de code, collé une fois
StyleLes styles et états du formulaire FramerDes styles en ligne que vous modifiez, ou vos propres classes
Limite de débit par visiteurNon. Tous les envois partagent les IP de FramerOui, 5 par minute chacun
Domaines autorisésLaisser videRéglés sur vos domaines Framer et personnalisé
HoneypotNonOui
Vérification humaineDésactivéeDésactivée, car il envoie avec fetch
Suivi de la sourceAucune?ref= dans l'URL, ou une valeur par défaut par instance

La voie webhook convient à une page que vous partagez avec quelques centaines de personnes. Si vous êtes sur le point de publier la page quelque part de très visible, utilisez le code component : une vague d'inscriptions de bots via les serveurs de Framer ne peut être arrêtée que par le score de spam.

Voie 1 : le formulaire Framer avec un webhook

  1. Ajoutez à la page un formulaire avec un input Email. Dans les réglages de l'input, nommez-le email. Framer envoie le nom de chaque input comme clé JSON, et Ikiform utilise email pour l'adresse de réponse et la vérification des domaines jetables.
  2. Sélectionnez le formulaire. À côté de Send To, choisissez Add… › Webhook et collez votre endpoint, https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx. Le secret de signature est facultatif ; Ikiform ne vérifie pas la signature de Framer.
  3. Dans Ikiform, laissez Allowed domains vide et la vérification anti-spam désactivée. Avec l'un ou l'autre activé, chaque envoi Framer est signalé ou refusé, car il n'a pas d'origine et ne peut pas exécuter de vérification dans un navigateur.
  4. Réglez la limite de débit sur Relaxed, ou Custom le jour du lancement. Standard compte tous les envois de Framer comme un seul visiteur, et le formulaire entier est plafonné à 300 par minute.
  5. Stylez l'état de succès du formulaire dans Framer. Il s'affiche quand Ikiform répond 201.
Un envoi refusé par Ikiform, comme un e-mail manquant avec une définition de champs en place, échoue de la même façon lors des cinq tentatives de Framer. Vérifiez la livraison dans Framer si les inscriptions cessent d'arriver.

Voie 2 : un code component

Dans le panneau Assets, ouvrez Code, créez un fichier de code nommé WaitlistSignup et collez ceci. Faites-le glisser sur la page et définissez Form ID dans le panneau des propriétés.

WaitlistSignup.tsx
import { addPropertyControls, ControlType } from "framer";
import { type CSSProperties, type FormEvent, useRef, useState } from "react";

const box: CSSProperties = {
  font: "inherit",
  padding: "12px 14px",
  borderRadius: 10,
  border: "1px solid rgba(0, 0, 0, 0.15)",
};

interface Props {
  formId: string;
  buttonLabel: string;
  joinedText: string;
  source: string;
}

export default function WaitlistSignup({ formId, buttonLabel, joinedText, source }: Props) {
  const [status, setStatus] = useState<"idle" | "sending" | "joined">("idle");
  const [error, setError] = useState("");
  const key = useRef("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    const ref = new URLSearchParams(window.location.search).get("ref");
    body.set("ref", (ref || source).slice(0, 40));
    key.current ||= crypto.randomUUID();
    setStatus("sending");
    setError("");
    try {
      const response = await fetch(`https://ikiform.com/f/${formId}`, {
        method: "POST",
        headers: { Accept: "application/json", "Idempotency-Key": key.current },
        body,
      });
      const result = await response.json();
      if (result.ok) return setStatus("joined");
      setError(result.error.fields?.email?.[0] ?? result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    }
    setStatus("idle");
  }

  if (status === "joined") return <p role="status" style={{ margin: 0 }}>{joinedText}</p>;

  return (
    <form onSubmit={onSubmit} style={{ display: "flex", flexWrap: "wrap", gap: 8, width: "100%" }}>
      <input
        name="email"
        type="email"
        placeholder="you@company.com"
        aria-label="Email"
        autoComplete="email"
        required
        style={{ ...box, flex: "1 1 220px" }}
      />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      <button type="submit" disabled={status === "sending"} style={{ ...box, cursor: "pointer" }}>
        {status === "sending" ? "Joining" : buttonLabel}
      </button>
      {error ? <p role="alert" style={{ flexBasis: "100%", margin: 0 }}>{error}</p> : null}
    </form>
  );
}

addPropertyControls(WaitlistSignup, {
  formId: { type: ControlType.String, title: "Form ID", defaultValue: "frm_xxxxxxxxxxxxxxxx" },
  buttonLabel: { type: ControlType.String, title: "Button", defaultValue: "Join the waitlist" },
  joinedText: { type: ControlType.String, title: "Joined text", defaultValue: "You're on the list." },
  source: { type: ControlType.String, title: "Source", defaultValue: "framer" },
});
  • Le panneau des propriétés expose le libellé du bouton, le texte de confirmation et une Source par défaut. Placez une instance dans le hero avec hero et une autre dans le pied de page avec footer, et vous verrez laquelle convertit.
  • Un ?ref= dans l'URL de la page l'emporte sur la valeur par défaut : les liens de campagne se taguent donc tout seuls.
  • La clé d'idempotence est créée au premier envoi et réutilisée lors des nouvelles tentatives de la même visite : un double appui sur un téléphone ne stocke donc qu'une inscription.
  • window n'est lu que dans le gestionnaire d'envoi. Framer rend d'abord les composants côté serveur, et toucher à window pendant le rendu casserait cela.

Réglages Ikiform pour le composant

Ajoutez ceci dans l'onglet Fields du formulaire, pour que l'e-mail soit obligatoire et ref autorisé :

Champs
{
  "version": 1,
  "title": "Waitlist",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}
  • Dans Allowed domains, ajoutez your-project.framer.app et votre domaine personnalisé. N'ajoutez jamais framer.app seul ; tous les sites Framer correspondraient.
  • Gardez la vérification anti-spam désactivée. Le composant envoie avec fetch sans récupérer de jeton : un formulaire avec la vérification activée répond donc 403 challenge_required.
  • Gardez les limites de débit Standard. Chaque visiteur est désormais compté séparément.

Vérifiez que tout fonctionne

  1. Publiez le site. Les webhooks et les code components n'envoient que depuis le site publié, pas depuis l'aperçu de l'éditeur.
  2. Envoyez le formulaire sur votre domaine framer.app, puis à nouveau sur votre domaine personnalisé.
  3. Ouvrez Submissions dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
  4. Pour le composant, ouvrez la page avec ?ref=test et vérifiez que l'inscription affiche ref à test.
  5. 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.
  6. 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.

Pièges

ProblèmeQue faire
Toutes les inscriptions par webhook sont en spam avec origin_mismatchAllowed domains est défini, et les envois des serveurs de Framer n'ont pas d'origine. Videz-le pour ce formulaire, ou passez au code component.
Framer signale des livraisons de webhook échouéesIkiform a refusé l'envoi : 403 challenge_required avec la vérification anti-spam activée, ou 422 parce qu'un nom d'input n'est pas email. Corrigez la cause ; les nouvelles tentatives échouent de la même façon.
429 rate_limitedUn 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.
Le composant affiche Unknown field "ref"La définition des champs ne liste pas ref. Ajoutez-le comme dans la définition ci-dessus.
403 origin_not_allowed, ou tous les envois finissent en spamLe 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.

La configuration Framer complète, y compris la version formulaire de contact, est dans le guide Framer de la documentation. L'export de la liste et l'envoi de l'e-mail de lancement sont dans le guide principal.

Questions

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