Formulario de suscripción a newsletter

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.

Preet SutharFundador, Ikiform

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.

Dónde vive la API key de la herramienta de email en un sitio estáticoclave en la páginaclave en un relaypáginaestáticaAPI key en suJavaScriptAPI de laherramientacualquierapuedellamarlatu listala puede leercualquierapáginaestáticasolo endpointpúblicoIkiformfiltro despam, webhookfirmadorelayguarda laAPI keyherramientade emailañade elsuscriptorclaveenvíofirmadoclaveDónde vive la API key de la herramienta de email en un sitio estáticoclave en la páginaclave en un relaypágina estáticaAPI key en su JavaScriptAPI de la herramientacualquiera puede llamarlatu listala puede leer cualquierapágina estáticasolo endpoint públicoIkiformfiltro de spam, webhook firmadorelayguarda la API keyherramienta de emailañade el suscriptorclaveenvíofirmadoclave
  1. 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.
  2. Con un relay, la página solo conoce tu endpoint de Ikiform, que acepta envíos de formulario y nada más.
  3. Ikiform filtra el spam y envía un webhook firmado al relay.
  4. El relay, alojado fuera del sitio estático, guarda la API key y añade el suscriptor.
El endpoint del formulario se puede publicar sin problema. La API key, nunca.

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 source a 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 con unknown_field.
  • _redirect debe ser una URL https: 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.

static/newsletter.js
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 fetch falla, por ejemplo detrás de un filtro de red estricto, el script recurre a form.submit(), que envía de la forma normal y llega a /subscribed/.
  • Un 429 tiene 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áticoOpciones para el relay
GitHub Pages, S3Sin funciones. Usa un Cloudflare Worker en un dominio propio, o una herramienta de automatización
NetlifyUna Netlify Function en el dominio de tu sitio, o cualquiera de las anteriores
Cloudflare PagesUn Worker aparte en un dominio propio. Ikiform rechaza las URLs pages.dev y workers.dev
VercelUna 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.

Los sitios estáticos suelen tener previews de despliegue en un dominio aparte. Los envíos desde un dominio que no está en Allowed domains se marcan como spam o se rechazan, así que añade el dominio de preview mientras pruebas, o prueba en el dominio real y en 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:

_headers
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com

Un 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

  1. Añade tu dominio real y localhost en Allowed domains.
  2. Haz el build del sitio y abre una página de post. Están los dos formularios, cada uno con su propio valor source en el código fuente de la página.
  3. 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.
  4. Desactiva JavaScript y suscríbete desde el footer. Llegas a /subscribed/.
  5. Abre Submissions en el panel. Las dos suscripciones muestran post y footer como su source.
  6. Busca en el sitio generado la API key de tu herramienta de email. No está.

Errores comunes

ProblemaQué hacer
Nada en el panel, y un error de CSP en la consolaAñade https://ikiform.com a form-action y connect-src en tu Content-Security-Policy.
422 con unknown_field para sourceAñ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 spamEl 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

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