Formulario de contacto en un sitio estático

Formulario de contacto en un sitio estático6 min de lectura

Añade un formulario de contacto en Netlify sin Netlify Forms

Usa un formulario de contacto en un sitio de Netlify sin Netlify Forms: sin detección en el build, envíos JSON desde React y un formulario aparte para las deploy previews.

Preet SutharFundador, Ikiform

Quita data-netlify del formulario y apunta su action a tu endpoint de Ikiform; así Netlify sirve la página y nunca toca el envío. Los formularios de React y Vue no necesitan una copia oculta en HTML y pueden enviar JSON. Define otro id de formulario para las deploy previews en netlify.toml, porque los hosts de preview no coinciden con tu sitio en Allowed domains.

Qué cambia en Netlify

Netlify es el único gran host estático con una función de formularios integrada. Netlify Forms lee tu HTML desplegado después de cada build, encuentra los formularios marcados con data-netlify o netlify y acepta envíos a tu propio sitio para esos formularios. Cualquier otro envío a una página estática falla, y en Netlify falla con 404, no con 405:

Terminal
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
Respuesta
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0

Ese 404 hace que la gente busque una página que falta y que no falta. Significa que ningún formulario detectado coincidió con el envío. Si usar Netlify Forms o no es otra cuestión, y la comparativa entre Ikiform y Netlify Forms la responde con honestidad: si tu sitio va a vivir siempre en Netlify y el formulario es HTML plano, Netlify Forms da menos trabajo. Esta página es para los demás casos.

Tu situaciónPor qué estorba Netlify Forms
El formulario lo renderiza React, Vue o SvelteEl build analiza HTML estático, así que un formulario renderizado en el cliente necesita un duplicado oculto para que Netlify lo encuentre
Quieres enviar JSON desde fetchNetlify Forms solo acepta cuerpos urlencoded y multipart
Puede que el sitio se mude a otro hostEl formulario envía a tu propio dominio, así que solo funciona mientras Netlify sirva ese dominio
Los mensajes deben llegar a un webhook que verificasIkiform firma cada entrega con cabeceras de Standard Webhooks y la reintenta
Netlify Forms e Ikiform lado a lado en el mismo sitio de Netlifycon Netlify Formscon IkiformbuildNetlify analiza elHTML¿data-netlify?POST a tu sitioNetlify lo guarda404 en el POSTformulario ignoradobuildsin análisisPOST aikiform.comHTML o JSONIkiformguarda, avisa, webhooksencontradono encontradoNetlify Forms e Ikiform lado a lado en el mismo sitio de Netlifycon Netlify Formscon IkiformbuildNetlify analiza elHTML¿data-netlify?POST a tu sitioNetlify lo guarda404 enel POSTformularioignoradobuildsin análisisPOST aikiform.comHTML o JSONIkiformguarda, avisa,webhooksencontradono encontrado
  1. Con Netlify Forms, Netlify analiza el HTML de cada despliegue en busca de formularios marcados con data-netlify.
  2. Un formulario que encontró acepta envíos a tu propio sitio, y Netlify los guarda.
  3. Un formulario que no encontró, como uno renderizado con JavaScript sin copia oculta, recibe 404 en cada envío.
  4. Con Ikiform, no hay nada que encontrar en el build.
  5. El formulario envía directamente a ikiform.com, como HTML o JSON, e Ikiform lo guarda y te envía un email.
Ambos pueden convivir en un sitio. Un formulario con data-netlify va a Netlify; un formulario cuyo action es un endpoint de Ikiform va a Ikiform.

Saca un formulario de Netlify Forms

  • Quita del formulario data-netlify o netlify, netlify-honeypot y data-netlify-recaptcha, y cualquier input oculto form-name. Si lo dejas, form-name se guarda como un campo más.
  • Borra la copia estática oculta de un formulario de JavaScript, como la de abajo. Netlify solo la necesitaba para detectar el formulario.
  • Pon tu endpoint de Ikiform en el action, o en la URL del fetch. El antiguo action="/thanks" pasa al ajuste Redirect after submit del formulario.
  • Descarga primero los envíos anteriores desde la página Forms de Netlify. No se mueven solos.
index.html, borra esto
<form name="contact" netlify hidden>
  <input name="name" />
  <input name="email" />
  <textarea name="message"></textarea>
</form>

Deja form detection activado si otros formularios del sitio siguen usando Netlify Forms. La documentación de Netlify dice que desactivar la detección es solo para sitios que no usan Netlify Forms. Cuando ya nada lo use, desactivarlo te ahorra un análisis en cada despliegue.

Qué sustituye a los ajustes antispam de Netlify

Netlify pasa Akismet por los envíos y ofrece netlify-honeypot y reCAPTCHA. En Ikiform, el honeypot sigue funcionando si pones en Honeypot field del formulario el nombre que ya usas, como bot-field. La puntuación de spam se aplica a cada envío sin configurar nada, y Spam check en Ikiform sustituye reCAPTCHA por Cloudflare Turnstile sin claves. Quita el div de reCAPTCHA al cambiar.

Un formulario de React que envía JSON

Sin Netlify Forms de por medio, un formulario renderizado en el cliente no necesita duplicado y puede enviar JSON, lo que conserva los tipos y te permite limpiar los valores antes. Este además registra qué contexto de Netlify generó la página:

src/ContactForm.tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = `https://ikiform.com/f/${import.meta.env.VITE_IKIFORM_FORM_ID}`;

interface Message {
  name: string;
  email: string;
  message: string;
  context: string;
}

export function ContactForm() {
  const [sent, setSent] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const body: Message & Record<string, string> = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      context: import.meta.env.VITE_CONTEXT ?? "local",
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
    setError("");
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    }).catch(() => null);
    const result = await response?.json().catch(() => null);
    if (result?.ok) return setSent(true);
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    setError(Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.");
  }

  if (sent) return <p role="status">Got it. I'll answer from my own inbox, usually the same day.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} minLength={10} required />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button>Send</button>
    </form>
  );
}
  • Content-Type: application/json le dice a Ikiform que lea JSON; Accept: application/json pide una respuesta en JSON.
  • VITE_CONTEXT es un nombre de ejemplo. Netlify define CONTEXT durante el build; exponlo a Vite en tu comando de build, o quita el campo.
  • El honeypot se envía como una clave JSON. Mantenlo en el cuerpo aunque el input esté oculto, para que los bots que lo rellenan se sigan delatando.

Las deploy previews tienen su propio formulario

Las deploy previews viven en hosts como deploy-preview-12--your-site.netlify.app. Eso no es un subdominio de your-site.netlify.app, así que no coincide en Allowed domains, y los envíos desde previews acaban en spam. Dale a las previews y a los branch deploys su propio formulario en netlify.toml, y define la cabecera Content-Security-Policy en el mismo archivo:

netlify.toml
[build]
  command = "npm run build"
  publish = "dist"

[context.production.environment]
  VITE_IKIFORM_FORM_ID = "frm_xxxxxxxxxxxxxxxx"

[context.deploy-preview.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[context.branch-deploy.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"

Deja vacío Allowed domains del formulario de preview, o añade el host de preview exacto mientras pruebas. El formulario de producción lleva tu dominio y your-site.netlify.app. Las cabeceras de netlify.toml se aplican a todos los contextos de despliegue; si prefieres un archivo _headers, tiene que acabar en el directorio de publicación.

Nunca añadas netlify.app por sí solo a Allowed domains. Coincidiría cualquier sitio de Netlify, incluidas las copias de tu formulario en sitios de otras personas.

Comprueba que funciona

  1. Abre una deploy preview, envía y confirma que el mensaje llega al formulario de preview.
  2. Haz merge y envía en producción. Confirma que llega al formulario de producción y que la página Forms de Netlify no muestra nada nuevo.
  3. Revisa la bandeja de entrada de cada destinatario de notificaciones. Cada uno recibe un email con los campos, y Reply va a quien envió el formulario cuando este tiene un campo email.
  4. Envía con el honeypot relleno, _gotcha=bot. Sigue recibiendo una respuesta de éxito, pero el envío se guarda como spam y no manda ningún email ni webhook.
  5. ¿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
404 al enviarEl envío fue a tu propio sitio. Puede que un build en caché aún tenga el action antiguo, o que una llamada fetch siga enviando a /. Busca form-name en el directorio de publicación.
Los envíos siguen apareciendo en NetlifyAlgún formulario de la página sigue enviando a tu propio sitio con data-netlify o netlify, así que Netlify sigue detectándolo y guardándolo. Quita el atributo, pon Ikiform en el action y vuelve a desplegar.
404 not_found de Ikiform solo en las previewsEl contexto de deploy preview no tiene VITE_IKIFORM_FORM_ID, así que la URL termina en undefined. Añádelo en [context.deploy-preview.environment].
403 origin_not_allowed, o todos los envíos acaban en spamEl dominio de la página no está en Allowed domains del formulario. Añádelo, más localhost para las pruebas locales. Los subdominios de un dominio de la lista están permitidos.
422 files_not_supportedEl formulario tiene un <input type="file"> con un archivo elegido. La subida de archivos aún no está disponible; quita el input o pide un enlace.

Preguntas

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