Formulario de contacto en un sitio estático

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

Soluciona el error 405 en un formulario de contacto alojado en Vercel

Una página estática en Vercel responde al POST de un formulario con 405 Method Not Allowed; aquí verás por qué y cómo enviar el formulario a un backend.

Preet SutharFundador, Ikiform

Vercel sirve tu build estático como archivos, y un POST a un archivo recibe 405 con el cuerpo vacío porque ninguna función atiende esa ruta. Apunta el action del formulario a tu endpoint de Ikiform, o escribe una Vercel Function que acepte el envío. Con Ikiform, define el id del formulario por entorno de Vercel para que las previews envíen a un formulario de prueba y producción al real.

Por qué Vercel responde 405

Vercel dirige cada petición a un archivo estático o a una función. Un sitio estático, un build de Vite o una app de Next.js con output: "export" solo tiene archivos. Un GET a /contact devuelve el archivo; un POST a la misma ruta no tiene a dónde ir, así que Vercel lo rechaza antes de que pueda ejecutarse nada de tu código:

Terminal
curl -i -X POST -d "message=hello" https://your-project.vercel.app/contact
Respuesta
HTTP/2 405
server: Vercel
content-disposition: inline
x-vercel-cache: HIT
content-length: 0

La respuesta no tiene cuerpo, así que el navegador muestra una página en blanco o su propia pantalla de error. Tres configuraciones llevan a esto:

Tu códigoPor qué da 405 en Vercel
<form method="post"> sin actionEl formulario envía a la página en la que está, que es un archivo estático
action="/api/contact" en un proyecto de Vite o de HTML planoNo hay ningún archivo en una carpeta api/ en la raíz del proyecto, así que no existe ninguna función para esa ruta
Una Server Action o un Route Handler de Next.js en una exportación estáticaoutput: "export" no admite Server Actions ni Route Handlers que leen la petición. next dev informa del error, y el build no tiene servidor para ejecutarlos
Cómo decide Vercel qué hacer con el POST de un formularioaction apunta a tu sitio en Vercelaction apunta a IkiformPOST/contactdesde elformularioenrutado deVercel¿archivo ofunción?¿hayfunción?tu funciónsi escribisteuna405, cuerpovacíoun archivoestáticoPOST aikiform.commismoformularioIkiformguarda, teavisapágina degraciasun archivoestático enVercelsíno303Cómo decide Vercel qué hacer con el POST de un formularioaction apunta a tu sitio en Vercelaction apunta a IkiformPOST /contactdesde el formularioenrutado deVercel¿archivo o función?¿hayfunción?tu funciónsi escribiste una405,cuerpovacíoun archivoestáticoPOST aikiform.commismo formularioIkiformguarda, te avisapágina degraciasun archivo estáticoen Vercelsí303no
  1. El formulario envía a una ruta de tu despliegue en Vercel.
  2. Vercel busca una función que atienda esa ruta.
  3. Si escribiste una, se ejecuta. En un build estático no hay ninguna, así que Vercel responde 405 con el cuerpo vacío.
  4. Con el action apuntando a Ikiform, el envío nunca llega a Vercel.
  5. Ikiform guarda el mensaje, te envía un email y redirige el navegador de vuelta a una página estática de tu sitio en Vercel.
Vercel no está roto. Un despliegue estático no tiene código para un POST, y una función es la única forma de añadirlo en el lado de Vercel.

Así que o añades una función o mandas el envío a otro sitio. Una Vercel Function en api/contact.ts funciona, pero entonces necesitas una API de email, filtrado de spam y límites de frecuencia, y mantenerlos tú. Mandar el envío a Ikiform mantiene el despliegue estático. La solución en el HTML es un atributo:

Antes
<form method="post">
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
Después
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">

Si prefieres escribir la función

Una función es una opción razonable cuando el mensaje tiene que ir a algún lugar al que solo llega tu código. Calcula lo que implica antes de empezar:

  • Una cuenta en un proveedor de email, un dominio de envío verificado y una clave de API en las variables de entorno de Vercel.
  • Validación en el servidor, porque los atributos required del navegador son una comodidad, no una comprobación.
  • Filtrado de spam y límites de frecuencia, o un formulario de contacto que te reenvía cada bot de internet.
  • Un lugar donde guardar los mensajes cuando el proveedor de email está caído, y una forma de reenviarlos.

Un backend de formularios cubre esa lista por ti. Si aun así quieres tu propio código en el circuito, deja que el formulario envíe a Ikiform y recibe un webhook firmado en una función; tu propio servidor muestra el handler.

Un id de formulario por entorno de Vercel

Cada pull request en Vercel genera un despliegue de preview. Si las previews envían a tu formulario de producción, cada mensaje de prueba acaba en tu bandeja de entrada junto a los reales. Crea un segundo formulario de Ikiform para las previews y deja que las variables de entorno de Vercel elijan. En Settings › Environment Variables, añade el mismo nombre dos veces:

Variables de entorno de Vercel
VITE_IKIFORM_FORM_ID   Production   frm_xxxxxxxxxxxxxxxx
VITE_IKIFORM_FORM_ID   Preview      frm_yyyyyyyyyyyyyyyy

Vite sustituye los placeholders %NAME% de index.html por variables VITE_ en el build, así que un formulario HTML plano puede usarlas sin JavaScript. Vercel define VITE_VERCEL_ENV en los proyectos de Vite, y registra de qué despliegue vino un mensaje:

index.html
<form id="contact" action="https://ikiform.com/f/%VITE_IKIFORM_FORM_ID%" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" required></textarea>

  <input type="hidden" name="build" value="%VITE_VERCEL_ENV%" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
</form>
  • En un sitio estático, Vercel lee las variables de entorno en el build. Después de cambiar una, vuelve a desplegar, o el valor antiguo se queda en el HTML.
  • Vite deja el placeholder tal cual cuando la variable no existe, así que un id que falta se publica como %VITE_IKIFORM_FORM_ID% y todos los envíos fallan con 400 Bad Request antes de llegar al formulario. La comprobación de abajo lo detecta.
  • El id del formulario es público en cualquier caso. Solo las variables VITE_ llegan al navegador, y por eso ningún secreto debe ir en una.
src/contact.js
const form = document.querySelector("#contact");

if (form && form.action.includes("%VITE_")) {
  console.error("VITE_IKIFORM_FORM_ID is not set for this Vercel environment");
  form.querySelector("button").disabled = true;
}

¿Usas Next.js con output: "export"? Lee process.env.NEXT_PUBLIC_IKIFORM_FORM_ID en un componente de cliente; la guía principal tiene un formulario para exportación estática. Si tu app de Next.js sí se ejecuta en los servidores de Vercel, una Server Action puede enviar con una clave secreta: consulta la guía de Next.js.

Cabeceras y URL limpias

Si tu sitio define una Content-Security-Policy, ponla en vercel.json e incluye Ikiform. cleanUrls sirve thanks.html en /thanks, lo que da un destino de redirección más limpio:

vercel.json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "cleanUrls": true,
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
        }
      ]
    }
  ]
}

Previews y dominios permitidos

Las URL de preview como your-project-git-branch-team.vercel.app no son subdominios de your-project.vercel.app, así que no coinciden con él en Allowed domains. Los envíos desde ellas acaban en spam con origin_mismatch. Es otra razón para tener un formulario aparte para las previews: deja vacíos sus dominios permitidos, o añade el host de preview que estés probando.

Con Vercel Authentication activada y el alcance Standard Protection, que cubre todos los despliegues salvo los dominios de producción, las URL de preview quedan detrás de un login de Vercel. Solo tu equipo puede acceder a ellas, así que un formulario de preview sin dominios permitidos no queda expuesto al público. Revisa Settings › Deployment Protection antes de confiar en eso. Los envíos desde previews no son pruebas de localhost, así que cuentan para tu plan como cualquier envío real.

Comprueba que funciona

  1. Ejecuta el curl de arriba contra tu URL de producción antes de la solución y confirma el 405. Después de la solución, el action del formulario en el HTML desplegado empieza por https://ikiform.com/f/.
  2. Abre un despliegue de preview, envía y confirma que el mensaje llega al formulario de preview, no al de producción.
  3. Envía desde tu dominio de producción y comprueba que el campo build dice production.
  4. 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.
  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
Sigue dando 405 después de cambiar el formularioEl despliegue que estás probando se generó antes del cambio, o un handler de JavaScript envía a tu propia ruta con fetch. Busca /api/contact en los archivos generados.
400 Bad Request en todos los envíos, o 404 not_found de IkiformEl id del formulario no llegó al build, o es el id equivocado para ese entorno. Comprueba que la variable existe para ese entorno y vuelve a desplegar.
Las previews funcionan, pero los envíos de producción acaban en spamFalta tu dominio de producción en Allowed domains del formulario de producción. Añade el dominio personalizado además de your-project.vercel.app.
El navegador llega a la página de agradecimiento de Ikiform en lugar de a la tuya_redirect se ignora salvo que su dominio esté en Allowed domains y sea una URL https:. Añade el dominio, o configura Redirect after submit en los ajustes del formulario.

Preguntas

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