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.
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:
curl -i -X POST -d "message=hello" https://your-project.vercel.app/contactHTTP/2 405
server: Vercel
content-disposition: inline
x-vercel-cache: HIT
content-length: 0La 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ódigo | Por qué da 405 en Vercel |
|---|---|
<form method="post"> sin action | El 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 plano | No 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ática | output: "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 |
- El formulario envía a una ruta de tu despliegue en Vercel.
- Vercel busca una función que atienda esa ruta.
- Si escribiste una, se ejecuta. En un build estático no hay ninguna, así que Vercel responde 405 con el cuerpo vacío.
- Con el action apuntando a Ikiform, el envío nunca llega a Vercel.
- 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.
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:
<form method="post">
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button>Send</button>
</form><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
requireddel 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:
VITE_IKIFORM_FORM_ID Production frm_xxxxxxxxxxxxxxxx
VITE_IKIFORM_FORM_ID Preview frm_yyyyyyyyyyyyyyyyVite 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:
<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 con400 Bad Requestantes 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.
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:
{
"$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.
localhost, así que cuentan para tu plan como cualquier envío real.Comprueba que funciona
- Ejecuta el
curlde arriba contra tu URL de producción antes de la solución y confirma el405. Después de la solución, elactiondel formulario en el HTML desplegado empieza porhttps://ikiform.com/f/. - Abre un despliegue de preview, envía y confirma que el mensaje llega al formulario de preview, no al de producción.
- Envía desde tu dominio de producción y comprueba que el campo
builddiceproduction. - 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. - ¿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
| Problema | Qué hacer |
|---|---|
Sigue dando 405 después de cambiar el formulario | El 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 Ikiform | El 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 spam | Falta 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
El formulario envía a una ruta que solo tiene un archivo estático. Vercel sirve archivos para peticiones GET y, sin una función para esa ruta, rechaza el POST con 405. Envía el formulario a un backend de formularios o añade una Vercel Function.
No. Las Server Actions no funcionan con output: export, porque no hay servidor que las ejecute. Envía desde un componente de cliente a un backend de formularios, o despliega sin la exportación estática para que Vercel ejecute la action.
Mejor que no. Dale a las previews su propio id de formulario mediante una variable de entorno de Preview, para que los mensajes de prueba no lleguen a tu bandeja de entrada ni a tus webhooks.
No. El formulario funciona sin él. Añade vercel.json solo para definir una Content-Security-Policy que permita ikiform.com, o para activar cleanUrls y tener una URL de agradecimiento más limpia.
No, no hay plan gratuito. Probar es gratis: puedes enviar 50 envíos de prueba al mes desde localhost antes de pagar. Para pasar a producción, Pro empieza en 6 $/mes por 500 envíos, o puedes pagar una sola vez 10.000 envíos por 49 $ o 50.000 envíos por 149, $ con créditos que no caducan nunca.
¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.