Lista de espera para un nuevo SaaS

Lista de espera para un nuevo SaaS7 min de lectura

Cómo añadir una lista de espera a un sitio de Framer

Dos formas de recoger registros de lista de espera en Framer: el formulario de Framer con un webhook a Ikiform, o un code component que conserva los límites por visitante y el antispam.

Preet SutharFundador, Ikiform

Los formularios de Framer no pueden enviar a una URL externa, pero sí pueden mandar cada envío a un webhook, así que la vía sin código es el formulario de Framer con tu endpoint de Ikiform como webhook. Para un lanzamiento público, usa mejor un pequeño code component: el navegador envía directamente, así que el límite de frecuencia por visitante, los dominios permitidos y el honeypot de Ikiform siguen funcionando.

En Framer se crean muchas páginas previas al lanzamiento, porque la página en sí lleva una tarde. El formulario de lista de espera es donde la cosa se complica. El bloque de formulario de Framer no puede cambiar su action, así que eliges entre dos vías que se comportan distinto en cuanto llega tráfico real.

Qué cambia en Framer

  • Sin action personalizada. Un formulario de Framer envía a los destinos de Framer: su propio almacenamiento, el email o un Webhook. Un webhook es la única forma de llegar a un backend externo sin código.
  • Los webhooks salen de los servidores de Framer, no del visitante. Ikiform ve las direcciones IP de Framer y ninguna cabecera Origin. Eso desactiva tres protecciones: los dominios permitidos, el límite de frecuencia por visitante y la verificación humana.
  • Reintentos. Framer trata cualquier cosa que no sea un 2xx como un fallo y reintenta hasta cinco veces. No sigue redirecciones. Ikiform responde a un envío JSON con 201, así que un registro guardado es un éxito a la primera.
  • Los code components son React de verdad. Se ejecutan en el navegador del visitante en el sitio publicado, así que un formulario dentro de uno envía como cualquier otra página web.
Dos formas en que un registro de lista de espera en Framer llega a IkiformFormulario de Framer con webhookCode componentFormulario deFrameren tu páginaServidores deFramerJSON, hasta 5reintentosIkiformve la IP de Framer,sin origencode componentformulario React entu páginanavegador delvisitantefetch con clave deidempotenciaIkiformve al visitante y tudominioenviarwebhookenviarenvíoDos formas en que un registro de lista de espera en Framer llega a IkiformFormulario de Framer con webhookCode componentFormulario de Frameren tu páginaServidores de FramerJSON, hasta 5 reintentosIkiformve la IP de Framer, sin origencode componentformulario React en tu páginanavegador del visitantefetch con clave de idempotenciaIkiformve al visitante y tu dominioenviarwebhookenviarenvío
  1. Con la vía del webhook, el visitante envía el formulario de Framer y los servidores de Framer reenvían los campos a Ikiform como JSON.
  2. Ikiform ve a los servidores de Framer como remitente, así que no puede distinguir a los visitantes ni comprobar en qué sitio está el formulario.
  3. Con el code component, el navegador del visitante envía directamente a Ikiform.
  4. Ikiform ve la IP de cada visitante y tu dominio, así que se aplican todas las protecciones.
Ambas vías guardan el mismo registro. Se diferencian en quién cree Ikiform que lo envió.

Elige una vía

ItemFormulario de Framer con webhookCode component
ConfiguraciónDos campos en la barra lateral derechaUn archivo de código, pegado una vez
EstilosLos estilos y estados del formulario de FramerEstilos inline que editas, o tus propias clases
Límite de frecuencia por visitanteNo. Todos los envíos comparten las IP de FramerSí, 5 por minuto cada uno
Dominios permitidosDejar vacíoCon tus dominios de Framer y personalizado
HoneypotNoSí
Verificación humanaDesactivadaDesactivada, porque envía con fetch
Seguimiento del origenNinguna?ref= de la URL, o un valor por defecto por instancia

La vía del webhook está bien para una página que compartes con unos cientos de personas. Si vas a publicar la página en un sitio grande, usa el code component: una ráfaga de registros de bots a través de los servidores de Framer solo se puede frenar con la puntuación de spam.

Vía 1: el formulario de Framer con un webhook

  1. Añade a la página un formulario con un input Email. En los ajustes del input, pon email como nombre. Framer envía el nombre de cada input como clave JSON, e Ikiform usa email para la dirección de respuesta y la comprobación de dominios desechables.
  2. Selecciona el formulario. Junto a Send To, elige Add… › Webhook y pega tu endpoint, https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx. El secreto de firma es opcional; Ikiform no comprueba la firma de Framer.
  3. En Ikiform, deja Allowed domains vacío y la verificación antispam desactivada. Con cualquiera de las dos activa, cada envío de Framer se marca o se rechaza, porque no lleva origen y no puede ejecutar una verificación en el navegador.
  4. Pon el límite de frecuencia en Relaxed, o en Custom el día del lanzamiento. Standard cuenta todos los envíos de Framer como un solo visitante, y todo el formulario tiene un tope de 300 por minuto.
  5. Dale estilo al estado de éxito del formulario en Framer. Aparece cuando Ikiform responde 201.
Un envío que Ikiform rechaza, como uno sin email cuando hay una definición de campos, falla igual en los cinco reintentos de Framer. Revisa la entrega en Framer si dejan de llegar registros.

Vía 2: un code component

En el panel Assets, abre Code, crea un archivo de código llamado WaitlistSignup y pega esto. Arrástralo a la página y configura Form ID en el panel de propiedades.

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" },
});
  • El panel de propiedades expone el texto del botón, el texto tras unirse y un Source por defecto. Pon una instancia en el hero con hero y otra en el footer con footer, y verás cuál convierte.
  • Un ?ref= en la URL de la página tiene prioridad sobre el valor por defecto, así que los enlaces de campaña se etiquetan solos.
  • La clave de idempotencia se crea en el primer envío y se reutiliza en los reintentos de la misma visita, así que un doble toque en un teléfono guarda un solo registro.
  • window solo se lee dentro del handler de envío. Framer renderiza primero los componentes en el servidor, y tocar window durante el renderizado lo rompería.

Ajustes de Ikiform para el componente

Añade esto en la pestaña Fields del formulario, para que el email sea obligatorio y ref esté permitido:

Campos
{
  "version": 1,
  "title": "Waitlist",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}
  • En Allowed domains, añade your-project.framer.app y tu dominio personalizado. Nunca añadas framer.app solo; coincidiría con todos los sitios de Framer.
  • Deja desactivada la verificación antispam. El componente envía con fetch y no pide un token, así que un formulario con la verificación activa responde 403 challenge_required.
  • Deja los límites de frecuencia en Standard. Ahora cada visitante se cuenta por separado.

Comprueba que funciona

  1. Publica el sitio. Los webhooks y los code components solo envían desde el sitio publicado, no desde la vista previa del editor.
  2. Envía el formulario en tu dominio framer.app y otra vez en tu dominio personalizado.
  3. Abre Submissions en el panel. El nuevo envío está ahí con todos los campos que mandaste.
  4. Para el componente, abre la página con ?ref=test y comprueba que el registro muestra ref como test.
  5. Revisa la bandeja de entrada de cada destinatario de las notificaciones. Cada uno recibe un email con los campos, y Reply responde a quien envió el formulario cuando este tiene un campo email.
  6. ¿No llegó nada? Cambia el filtro de estado del panel a Spam. El spam recibe la misma respuesta de éxito que un envío real.

Problemas habituales

ProblemaQué hacer
Todos los registros del webhook están en spam con origin_mismatchAllowed domains está configurado, y los envíos desde el servidor de Framer no tienen origen. Vacíalo para este formulario, o cambia al code component.
Framer informa de entregas de webhook fallidasIkiform rechazó el envío: 403 challenge_required con la verificación antispam activa, o 422 porque el nombre de un input no es email. Corrige la causa; los reintentos fallan igual.
429 rate_limitedUn visitante envió más de lo que permite el límite de frecuencia del formulario. Standard permite 5 por minuto por visitante. Espera lo que indique Retry-After, o elige Relaxed o Custom para formularios que envían desde una IP compartida.
El componente muestra Unknown field "ref"La definición de campos no incluye ref. Añádelo como en la definición de arriba.
403 origin_not_allowed, o todos los envíos acaban en spamEl dominio de la página no está en los Allowed domains del formulario. Añádelo, y también localhost para las pruebas locales. Los subdominios de un dominio de la lista están permitidos.

La configuración completa de Framer, incluida la versión de formulario de contacto, está en la guía de Framer de la documentación. Exportar la lista y enviar el email de lanzamiento están en la guía principal.

Preguntas

¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.