Formulario de suscripción a newsletter6 min de lectura
Formulario de suscripción por email en un sitio estático, sin servidor
Añade un formulario de suscripción a tu newsletter en un sitio estático con Hugo, Jekyll, Eleventy o Astro, deja la API key fuera de la página y confirma en la herramienta.
En un sitio estático, el formulario envía desde el navegador del lector directamente a Ikiform, porque no hay ningún servidor tuyo al que enviar. Deja la API key de tu herramienta de email completamente fuera del sitio: el navegador solo conoce tu endpoint público de Ikiform, y un relay alojado en otro lugar añade el suscriptor. Escribe el formulario una vez como partial o include, pasa un source distinto en cada ubicación y añade un pequeño script para la versión sin recarga.
Qué cambia aquí
Un sitio estático son archivos en un hosting: GitHub Pages, Netlify, Cloudflare Pages, un bucket de S3. Nada de lo que escribas se ejecuta en un servidor cuando un lector envía un formulario. Eso descarta el atajo habitual de llamar a la API de tu herramienta de email desde la página, porque todo lo que ejecuta el navegador lo puede leer cualquiera.
Las API keys de las herramientas de email rara vez se limitan a añadir un suscriptor. Una clave que puede añadir un suscriptor normalmente también puede leer, exportar o borrar toda la lista. Si la pones en el JavaScript de un sitio estático, se envía a cada visitante, y de ahí a cualquiera que abra las herramientas de desarrollo.
- Con la clave en la página, cada visitante la descarga, y cualquiera puede llamar con ella a la API de la herramienta de email.
- Con un relay, la página solo conoce tu endpoint de Ikiform, que acepta envíos de formulario y nada más.
- Ikiform filtra el spam y envía un webhook firmado al relay.
- El relay, alojado fuera del sitio estático, guarda la API key y añade el suscriptor.
Escribe el formulario una vez
Un formulario de newsletter aparece en varias plantillas: el footer, el final de cada post, una página de newsletter. Conviértelo en un partial que reciba un valor source, para que cada ubicación se registre sola. El markup es idéntico en cada generador; solo cambia la sintaxis de la plantilla para source.
<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
<label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
<input type="hidden" name="source" value="{{ .source | default "footer" }}" />
<input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Subscribe</button>
<p role="status" hidden></p>
</form>Luego ponlo en una plantilla con el nombre de la ubicación:
{{ partial "newsletter.html" (dict "source" "post") }}- En Astro, usa
<Newsletter source="post" />. Un build estático de Astro no puede ejecutar Actions, así que el componente envía directamente a Ikiform como los demás. - Añade
sourcea la definición de campos del formulario en Ikiform, como texto con una longitud máxima corta. Con campos definidos, un input que la definición no incluye se rechaza conunknown_field. _redirectdebe ser una URLhttps:en uno de los Allowed domains del formulario. Crea la página:/subscribed/, con una línea que pida a los lectores revisar su bandeja de entrada.
Un script para todos los formularios de la página
Una página de post puede tener dos formularios de newsletter, uno después del artículo y otro en el footer. Este script gestiona todos los formularios marcados con data-newsletter, y cuando uno tiene éxito los sustituye todos, para que a nadie se le vuelva a pedir que se suscriba unos centímetros más abajo. Cárgalo una vez desde tu layout base.
for (const form of document.querySelectorAll("form[data-newsletter]")) {
const status = form.querySelector('[role="status"]');
const button = form.querySelector("button");
form.addEventListener("submit", async (event) => {
event.preventDefault();
button.disabled = true;
status.hidden = false;
status.textContent = "Subscribing";
try {
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const result = await response.json();
if (result.ok) {
for (const other of document.querySelectorAll("form[data-newsletter]")) {
other.replaceChildren(Object.assign(document.createElement("p"), {
textContent: "Almost done. Check your inbox for a confirmation link.",
}));
}
return;
}
if (response.status === 429 && response.headers.has("Retry-After")) {
status.textContent = "Too many tries from this network. Wait a minute and try again.";
} else {
const errors = result.error.fields ?? {};
status.textContent = errors.email?.[0] ?? errors.consent?.[0] ?? result.error.message;
}
} catch {
form.submit();
return;
}
button.disabled = false;
});
}- Si
fetchfalla, por ejemplo detrás de un filtro de red estricto, el script recurre aform.submit(), que envía de la forma normal y llega a/subscribed/. - Un
429tiene su propio mensaje. Los lectores de una misma red de oficina o campus comparten IP y pueden alcanzar juntos el límite por visitante. - El formulario funciona con JavaScript desactivado. El script solo cambia cómo se muestra la respuesta.
Los lectores con JavaScript desactivado llegan a /subscribed/, así que haz que esa página haga lo que el script hace para todos los demás: decir que hay un email de confirmación en camino, indicar el remitente que deben buscar, mencionar la carpeta de spam y enlazar de vuelta al blog. Es una página normal de tu generador, construida como cualquier otra, y también es adonde puedes apuntar después el enlace de confirmación de la herramienta de email.
Dónde vive el relay
El relay necesita una URL HTTPS pública que ejecute código. Puede que tu hosting estático lo ofrezca o no:
| Hosting estático | Opciones para el relay |
|---|---|
| GitHub Pages, S3 | Sin funciones. Usa un Cloudflare Worker en un dominio propio, o una herramienta de automatización |
| Netlify | Una Netlify Function en el dominio de tu sitio, o cualquiera de las anteriores |
| Cloudflare Pages | Un Worker aparte en un dominio propio. Ikiform rechaza las URLs pages.dev y workers.dev |
| Vercel | Una función en tu dominio. La salida estática puede convivir con una ruta de API |
El código del relay para cada herramienta de email está en las páginas de Kit, MailerLite y beehiiv. Sin nada de código, apunta el webhook a Zapier o n8n y usa su acción para tu herramienta de email. La documentación de webhooks lista las reglas de URL.
localhost.Si tu sitio envía una Content Security Policy
Muchos hostings estáticos te dejan definir cabeceras desde un archivo, como _headers en Netlify y Cloudflare Pages. Si el tuyo envía una Content-Security-Policy, el navegador bloquea el formulario a menos que la política permita Ikiform. form-action cubre el envío normal y connect-src cubre el fetch del script:
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.comUn envío bloqueado aparece como error de CSP en la consola del navegador y nunca llega a Ikiform, así que tampoco aparece nada en el panel.
Comprueba que funciona
- Añade tu dominio real y
localhosten Allowed domains. - Haz el build del sitio y abre una página de post. Están los dos formularios, cada uno con su propio valor
sourceen el código fuente de la página. - Suscríbete desde el formulario del post con una dirección tuya. Los dos formularios cambian a la línea que pide revisar la bandeja de entrada.
- Desactiva JavaScript y suscríbete desde el footer. Llegas a
/subscribed/. - Abre Submissions en el panel. Las dos suscripciones muestran
postyfootercomo susource. - Busca en el sitio generado la API key de tu herramienta de email. No está.
Errores comunes
| Problema | Qué hacer |
|---|---|
| Nada en el panel, y un error de CSP en la consola | Añade https://ikiform.com a form-action y connect-src en tu Content-Security-Policy. |
422 con unknown_field para source | Añade source a la definición de campos del formulario, o quita el input oculto. |
403 origin_not_allowed, o todos los envíos acaban en spam | El dominio de la página no está en Allowed domains del formulario. Añádelo, y localhost para las pruebas locales. Los subdominios de un dominio de la lista están permitidos. |
| El navegador acaba en la página de agradecimiento de Ikiform en lugar de 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
Sí. El formulario envía desde el navegador a un backend de formularios como Ikiform, que guarda y filtra cada suscripción. Un relay o una herramienta de automatización alojados en otro lugar la pasan a tu herramienta de email, así que el sitio estático no ejecuta código de servidor.
No lo hagas. Cualquier clave en el JavaScript de la página es visible para cada visitante, y las claves de las herramientas de email normalmente pueden leer o cambiar toda la lista. Guarda la clave en un relay que se ejecute fuera del sitio.
Sí. El formulario es HTML puro y envía a Ikiform. GitHub Pages no puede ejecutar el relay, así que alójalo en un Cloudflare Worker con un dominio propio, o usa una herramienta de automatización como Zapier o n8n.
Tu herramienta de email. Ikiform nunca envía email a quien se suscribe. El relay añade al lector como no confirmado y la herramienta envía su propio enlace de confirmación.
¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.