Has desplegado en GitHub Pages, Netlify o Cloudflare Pages. Añades un <form>, un visitante pulsa Enviar, y el envío no llega a ninguna parte porque no tienes un servidor que lo reciba. Ese es el problema: un formulario de contacto necesita un destino para sus peticiones POST y un alojamiento estático no lo proporciona.

Tienes tres opciones realistas: un mailto: , una función serverless con una API de email o un endpoint externo. Los veremos por orden de esfuerzo y explicaremos dónde falla cada uno.

Opción 1: enlace mailto:

La opción sin esfuerzo es prescindir del formulario:

<a href="mailto:[email protected]">Email me</a>

No cuesta nada y falla de tres maneras:

  • Los recolectores de spam leen tu HTML. Los bots rastrean las direcciones publicadas en texto y el spam empieza en pocos días. (Hemos escrito sobre qué funciona contra el rastreo por separado.)
  • Depende del cliente de correo del visitante. mailto: abre el programa predeterminado. En un portátil de trabajo o móvil suele ser Outlook sin configurar o nada. El visitante ve una pestaña vacía y abandona.
  • No tienes estructura. Sin campos obligatorios, asunto elegido ni formato uniforme. <form action="mailto:..."> es aún peor: los navegadores lo gestionan de forma desigual y algunos no lo admiten.

Conclusión: válido para una página personal donde no importe perder mensajes. No es un formulario de contacto.

Opción 2: función serverless y API de email

La opción manual: funciones serverless reciben POST y una API (Resend, Postmark, SES u otra) lo entrega a tu correo. El código es breve:

// netlify/functions/contact.js (or a Vercel function, or a Worker)
export default async function handler(req) {
  const { name, email, message } = await req.json();
  if (!email || !message) {
    return new Response("Missing fields", { status: 400 });
  }

  // Every email API looks roughly like this
  await fetch("https://api.email-provider.example/send", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.EMAIL_API_KEY}` },
    body: JSON.stringify({
      to: "[email protected]",
      subject: `Contact form: ${name || "someone"}`,
      reply_to: email,
      text: message,
    }),
  });

  return Response.json({ ok: true });
}

Funciona y es razonable si ya utilizas funciones. Pero calcula su coste real:

  • Una función que programar, desplegar y mantener cuando cambian los entornos de ejecución.
  • Una cuenta con un proveedor de email, normalmente con verificación de dominio para una entrega fiable.
  • Claves API que crear, guardar como variables de entorno y renovar.
  • El spam corre de tu cuenta. El primer bot que descubra el endpoint lo utilizará y tendrás que programar límites y trampas.
  • Todo eso se repite en cada sitio que publicas. El cuarto proyecto también necesita su propia función, clave y configuración.

Fíjate en lo ocurrido: tu sitio «estático y sin servidor» ahora tiene un servidor. Pequeño, pero debes mantenerlo.

Opción 3: endpoint de formulario

Un endpoint de formulario es una URL que acepta las peticiones POST de tu formulario y guarda los mensajes donde puedas verlos. Tu sitio sigue siendo estático; el servicio hace de servidor. Toda la integración cabe en un atributo:

<form data-mailcontact-form action="https://mailcontact.app/api/forms/your-form-id" method="POST">
  <input type="text" name="name" placeholder="Your name" />
  <input type="email" name="email" placeholder="[email protected]" required />
  <input type="text" name="subject" placeholder="Subject" />
  <textarea name="message" placeholder="What's on your mind?" required></textarea>
  <button type="submit">Send</button>
  <input name="company_website" type="text" tabindex="-1" autocomplete="off"
    style="position:absolute;left:-10000px" aria-hidden="true" />
</form>

Ya tienes un formulario funcional. Con el endpoint de mailcontact, la URL acepta datos de formulario estándar o JSON y exige un email y un message (el nombre y el asunto son opcionales), y responde {"ok": true}. CORS está abierto, así que funciona desde cualquier origen: GitHub Pages, Netlify, un bucket de S3 e incluso un archivo HTML local. Los envíos tienen límites por IP y cada mensaje llega a la bandeja de tu proyecto.

Para mantener a los visitantes en tu página después de enviar, utiliza el script alojado de MailContact en lugar del envío nativo. Gestiona la confirmación, los errores y la verificación automática e inserta la comprobación oculta cuando hace falta:

<!-- Add data-mailcontact-form to the form above. -->
<script defer src="https://mailcontact.app/forms/v1.js"
  crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Es adecuado si quieres facilitar contacto sin mantener nada. Confías en un servicio en vez de tu código. Si necesitas lógica propia, adjuntos o CRM, programa la función. Para contacto estático, el endpoint gana en tiempo, mantenimiento y claves API (ninguna). La guía de sitios estáticos explica los detalles propios del alojamiento.

Recibir avisos y separar proyectos

Elijas lo que elijas, el objetivo es leer mensajes. Un formulario que entrega a una bandeja que nunca miras es un mailto: con pasos adicionales.

Aquí ayuda mailcontact: cada proyecto tiene una bandeja y un canal avisa cuando llega algo: Discord, Slack, email o un webhook. La misma bandeja también incluye una dirección de email real ([email protected]) que funciona al instante, sin configurar DNS ni servidores de correo, de modo que el formulario y el enlace «o escríbenos» llegan al mismo lugar.

Cuando publiques el siguiente proyecto, crea otra bandeja en vez de dirigir otro formulario a tu correo personal. Hemos escrito sobre por qué cada proyecto merece su bandeja. Separar los mensajes permite gestionar cinco proyectos sin multiplicar el ruido.