Formulario de contacto AJAX
Los mismos campos de contacto, enviados con fetch como JSON. El visitante se queda en la página y ve un estado breve.
Qué estás copiando
Esta plantilla envía los mismos campos de contacto con fetch, así el visitante se queda en tu página y lee un estado corto.
El formulario sigue teniendo una acción y el método POST. Si JavaScript no se ejecuta, el navegador hace un envío normal.
Ese respaldo importa para navegadores antiguos y para quien bloquea los scripts. No mantienes dos formularios.
El fragmento principal envía JSON. Pone Content-Type en application/json y Accept en application/json.
El éxito es el booleano success en el JSON. Cuando es true, el fragmento muestra el mensaje de la respuesta, o un agradecimiento corto, y vacía el formulario.
Cuando es false, el fragmento muestra el mensaje de la respuesta. Formgong escribe ese mensaje en el idioma de _lang.
JSON y FormData
Un cuerpo JSON tiene que listar los campos que quieres enviar. El fragmento incluye access_key, _lang, name, email, phone, message y botcheck.
Si más adelante quitas el comentario de Turnstile, el camino JSON también copia cf-turnstile-response cuando ese campo tiene un valor.
El fragmento extra usa FormData en lugar de JSON. El navegador pone el tipo de contenido, incluido el boundary. No pongas Content-Type tú.
FormData recoge cada campo del formulario, incluido un token de Turnstile si el widget está dentro del formulario.
Los dos caminos envían Accept: application/json. Sin esa cabecera, un envío normal del navegador recibe la página de agradecimiento, no JSON.
No envíes a la vez un cuerpo JSON y un cuerpo multipart. Elige uno. Los dos fragmentos son alternativas, no capas.
Spam, Telegram y límites
Deja el honeypot con el nombre botcheck, vacío, fuera de la pantalla, con tabindex -1. Un honeypot relleno se guarda como spam y no se entrega.
Los nombres de los campos se quedan cortos para que Telegram pueda imprimirlos en negrita. No cambies email por una frase larga.
La clave de acceso se queda en la página. Pasarla a un secreto de servidor no hace el formulario más seguro, y este fragmento no tiene servidor.
En el plan gratuito, Telegram es inmediato y el email es un resumen diario. La bandeja guarda los envíos 30 días.
El plan gratuito incluye 300 envíos al mes. Un envío de spam no gasta uno de esos.
Esta vista previa no envía. Conecta el fragmento en tu sitio y luego manda una prueba desde esa página.
Formgong opera el servicio. Si necesitas ayuda con el producto, escribe a support@formgong.com.
Vista previa
Código del formulario
<form id="fg-ajax" action="https://formgong.com/submit" method="POST" style="position:relative">
<input type="hidden" name="access_key" value="fk_your_access_key">
<input type="hidden" name="_lang" value="es">
<label>Nombre <input type="text" name="name" autocomplete="name" required></label>
<label>Correo electrónico <input type="email" name="email" autocomplete="email" required></label>
<label>Teléfono <input type="tel" name="phone" autocomplete="tel"></label>
<label>Mensaje <textarea name="message" required></textarea></label>
<div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
<label>Deja este campo vacío <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
</div>
<!--
Opcional. Active Turnstile para este formulario, quite estas marcas de comentario y pegue la clave del sitio.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY" data-language="es"></div>
-->
<button type="submit">Enviar</button>
<p data-status role="status"></p>
</form>
<script>
(function () {
var form = document.getElementById("fg-ajax");
if (!form) return;
var status = form.querySelector("[data-status]");
form.addEventListener("submit", function (event) {
event.preventDefault();
var data = {
access_key: form.access_key.value,
_lang: form._lang.value,
name: form.name.value,
email: form.email.value,
phone: form.phone.value,
message: form.message.value,
botcheck: form.botcheck.value
};
var token = form.querySelector('[name="cf-turnstile-response"]');
if (token && token.value) data["cf-turnstile-response"] = token.value;
var body = JSON.stringify(data);
fetch("https://formgong.com/submit", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: body
}).then(function (res) { return res.json(); }).then(function (result) {
if (status) status.textContent = result && result.success ? (result.message || "Enviado. ¡Gracias!") : ((result && result.message) || "No se pudo enviar. Inténtalo de nuevo.");
if (result && result.success) form.reset();
}).catch(function () {
if (status) status.textContent = "No se pudo enviar. Inténtalo de nuevo.";
});
});
})();
</script>El mismo formulario con FormData en lugar de JSON
<form id="fg-ajax" action="https://formgong.com/submit" method="POST" style="position:relative">
<input type="hidden" name="access_key" value="fk_your_access_key">
<input type="hidden" name="_lang" value="es">
<label>Nombre <input type="text" name="name" autocomplete="name" required></label>
<label>Correo electrónico <input type="email" name="email" autocomplete="email" required></label>
<label>Teléfono <input type="tel" name="phone" autocomplete="tel"></label>
<label>Mensaje <textarea name="message" required></textarea></label>
<div aria-hidden="true" style="position:absolute;inset-inline-start:0;top:0;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)">
<label>Deja este campo vacío <input type="text" name="botcheck" tabindex="-1" autocomplete="off"></label>
</div>
<!--
Opcional. Active Turnstile para este formulario, quite estas marcas de comentario y pegue la clave del sitio.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY" data-language="es"></div>
-->
<button type="submit">Enviar</button>
<p data-status role="status"></p>
</form>
<script>
(function () {
var form = document.getElementById("fg-ajax");
if (!form) return;
var status = form.querySelector("[data-status]");
form.addEventListener("submit", function (event) {
event.preventDefault();
var body = new FormData(form);
fetch("https://formgong.com/submit", {
method: "POST",
headers: { Accept: "application/json" },
body: body
}).then(function (res) { return res.json(); }).then(function (result) {
if (status) status.textContent = result && result.success ? (result.message || "Enviado. ¡Gracias!") : ((result && result.message) || "No se pudo enviar. Inténtalo de nuevo.");
if (result && result.success) form.reset();
}).catch(function () {
if (status) status.textContent = "No se pudo enviar. Inténtalo de nuevo.";
});
});
})();
</script>Preguntas y respuestas
¿Por qué el formulario sigue teniendo una URL de acción?
Para que el envío siga funcionando cuando JavaScript está apagado. El script llama a preventDefault solo cuando se ejecuta.
¿Debo enviar JSON o FormData?
Los dos valen. JSON nombra cada campo de forma explícita. FormData es más corto cuando añades inputs después, porque lee el formulario.
La línea de estado se queda vacía. ¿Qué ha fallado?
Comprueba que Accept sea application/json, que la clave de acceso corresponda a un formulario tuyo y que la página se sirva por http o https, no como archivo local.
¿Este formulario puede subir un archivo?
En esta plantilla, no. Los campos de archivo necesitan los adjuntos activados y un token de Turnstile válido. Usa la documentación del panel para esa configuración.