# 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.

Publicado el 2026-10-25 · https://ikiform.com/es/guides/newsletter-signup/static-site

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.

Diagrama: Dónde vive la API key de la herramienta de email en un sitio estático

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`.

**Hugo** (`layouts/partials/newsletter.html`)

```html
<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>
```

**Jekyll** (`_includes/newsletter.html`)

```html
<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="{{ include.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>
```

**Eleventy** (`_includes/newsletter.njk`)

```html
<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 or "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>
```

**Astro** (`src/components/Newsletter.astro`)

```astro
---
interface Props {
  source?: string;
}
const { source = "footer" } = Astro.props;
---

<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} />
  <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:

**Hugo** (`layouts/_default/single.html`)

```html
{{ partial "newsletter.html" (dict "source" "post") }}
```

**Jekyll** (`_layouts/post.html`)

```html
{% include newsletter.html source="post" %}
```

**Eleventy** (`_includes/post.njk`)

```html
{% set source = "post" %}
{% include "newsletter.njk" %}
```

- 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`:

```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á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](https://ikiform.com/es/guides/newsletter-signup/kit.md), [MailerLite](https://ikiform.com/es/guides/newsletter-signup/mailerlite.md) y [beehiiv](https://ikiform.com/es/guides/newsletter-signup/beehiiv.md). Sin nada de código, apunta el webhook a [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) o [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md) y usa su acción para tu herramienta de email. La [documentación de webhooks](https://ikiform.com/es/docs/webhooks.md) 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`:

```text
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

| 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

### ¿Puedo añadir un formulario de suscripción a newsletter a un sitio estático sin backend?

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.

### ¿Puedo llamar a la API de mi herramienta de email directamente desde la página estática?

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.

### ¿Funciona en GitHub Pages?

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.

### ¿Quién envía el email de confirmación?

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.

## Más guías

- [Formulario de suscripción a newsletter](https://ikiform.com/es/guides/newsletter-signup.md): Crea un formulario de suscripción a tu newsletter con tu propio markup, registra el consentimiento, bloquea los registros de bots y pasa cada suscriptor a Kit, MailerLite, beehiiv o cualquier herramienta.
- [Doble opt-in](https://ikiform.com/es/guides/newsletter-signup/double-opt-in.md): Ikiform no envía emails de confirmación, así que el doble opt-in se hace en tu herramienta de email: un relay añade cada suscripción como no confirmada y la herramienta envía el enlace.
- [Kit (ConvertKit)](https://ikiform.com/es/guides/newsletter-signup/kit.md): Conecta tu propio formulario de newsletter con Kit mediante un relay que crea el suscriptor, lo añade a un formulario de Kit para el incentive email y etiqueta la ubicación.
- [beehiiv](https://ikiform.com/es/guides/newsletter-signup/beehiiv.md): Conecta tu propio formulario de suscripción con beehiiv: un relay crea cada suscripción con el doble opt-in activado, la ubicación como fuente UTM y la página como referrer.
- [MailerLite](https://ikiform.com/es/guides/newsletter-signup/mailerlite.md): Conecta tu propio formulario de suscripción con MailerLite: un relay añade a los lectores nuevos como no confirmados para el doble opt-in y nunca pide a un suscriptor que confirme dos veces.
- [Formulario de newsletter en Next.js](https://ikiform.com/es/guides/newsletter-signup/nextjs.md): Un formulario de newsletter en el footer de Next.js: una server action con comprobación de consentimiento y Turnstile que envía a Ikiform, más un route handler que añade a los lectores a Kit.
