# Formulir pendaftaran email di situs statis, tanpa server

> Tambahkan formulir pendaftaran newsletter ke situs statis Hugo, Jekyll, Eleventy, atau Astro, jauhkan API key alat email dari halaman, dan lakukan konfirmasi di alat itu.

Terbit 2026-10-25 · https://ikiform.com/id/guides/newsletter-signup/static-site

Di situs statis, formulir mengirim dari browser pembaca langsung ke Ikiform, karena tidak ada server milik Anda untuk dikirimi. Jauhkan API key alat email Anda dari situs sepenuhnya: browser hanya tahu endpoint Ikiform publik Anda, dan relay yang di-host di tempat lain menambahkan subscriber. Tulis formulir sekali sebagai partial atau include, teruskan `source` berbeda per penempatan, dan tambahkan satu script kecil untuk versi tanpa reload.

## Apa yang berbeda di sini

Situs statis adalah file di sebuah host: GitHub Pages, Netlify, Cloudflare Pages, bucket S3. Tidak ada kode Anda yang berjalan di server saat pembaca mengirim formulir. Itu menutup jalan pintas biasa berupa memanggil API alat email dari halaman, karena apa pun yang dijalankan browser bisa dibaca siapa pun.

API key alat email jarang dibatasi hanya untuk menambahkan satu subscriber. Key yang bisa menambahkan subscriber biasanya juga bisa membaca, mengekspor, atau menghapus seluruh daftar. Taruh di JavaScript situs statis dan key itu terkirim ke setiap pengunjung, dan dari sana ke siapa pun yang membuka developer tools.

Diagram: Di mana API key alat email disimpan pada situs statis

1. Dengan key di halaman, setiap pengunjung mengunduhnya, dan siapa pun bisa memanggil API alat email dengannya.
2. Dengan relay, halaman hanya tahu endpoint Ikiform Anda, yang hanya menerima form post dan tidak lebih.
3. Ikiform menyaring spam dan mengirim webhook bertanda tangan ke relay.
4. Relay, yang di-host di luar situs statis, memegang API key dan menambahkan subscriber.

Endpoint formulir aman untuk dipublikasikan. API key tidak pernah aman.

## Tulis formulirnya sekali

Formulir newsletter muncul di beberapa template: footer, akhir setiap artikel, halaman newsletter. Jadikan partial yang menerima nilai `source`, agar setiap penempatan mencatat dirinya sendiri. Markup-nya identik di setiap generator; hanya sintaks template untuk `source` yang berubah.

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

Lalu taruh di template dengan nama penempatannya:

**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" %}
```

- Di Astro, gunakan `<Newsletter source="post" />`. Build Astro statis tidak bisa menjalankan Actions, jadi komponen mengirim langsung ke Ikiform seperti yang lain.
- Tambahkan `source` ke definisi field formulir di Ikiform, sebagai teks dengan panjang maksimum pendek. Setelah field didefinisikan, input yang tidak tercantum di definisi ditolak dengan `unknown_field`.
- `_redirect` harus berupa URL `https:` di salah satu **Allowed domains** formulir. Buat halamannya: `/subscribed/` dengan baris yang meminta pembaca memeriksa inbox.

## Satu script untuk setiap formulir di halaman

Halaman artikel bisa punya dua formulir newsletter, satu setelah artikel dan satu di footer. Script ini menangani setiap formulir bertanda `data-newsletter`, dan saat satu berhasil, script mengganti semuanya, jadi tidak ada yang diminta berlangganan lagi beberapa senti di bawahnya. Muat sekali dari base layout Anda.

`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;
  });
}
```

- Jika `fetch` gagal, misalnya di balik filter jaringan yang ketat, script kembali ke `form.submit()`, yang mengirim dengan cara biasa dan mendarat di `/subscribed/`.
- `429` punya pesannya sendiri. Pembaca di satu jaringan kantor atau kampus berbagi IP dan bisa mencapai batas per pengunjung bersama-sama.
- Formulir bekerja saat JavaScript mati. Script hanya mengubah cara jawaban ditampilkan.

Pembaca dengan JavaScript mati mendarat di `/subscribed/`, jadi buat halaman itu melakukan apa yang dilakukan script untuk semua orang lain: menyatakan email konfirmasi sedang dalam perjalanan, menyebut nama pengirim yang perlu dicari, menyinggung folder spam, dan menautkan kembali ke blog. Itu halaman biasa di generator Anda, dibuat seperti halaman lainnya, dan juga tempat Anda bisa mengarahkan tautan konfirmasi alat email nantinya.

## Di mana relay berada

Relay butuh URL HTTPS publik yang menjalankan kode. Host statis Anda mungkin menyediakannya, mungkin juga tidak:

| Host statis | Opsi relay |
| --- | --- |
| GitHub Pages, S3 | Tanpa function. Gunakan Cloudflare Worker di domain kustom, atau alat otomasi |
| Netlify | Netlify Function di domain situs Anda, atau salah satu opsi di atas |
| Cloudflare Pages | Worker terpisah di domain kustom. Ikiform menolak URL `pages.dev` dan `workers.dev` |
| Vercel | Function di domain Anda. Output statis bisa berdampingan dengan route API |

Kode relay untuk setiap alat email ada di halaman [Kit](https://ikiform.com/id/guides/newsletter-signup/kit.md), [MailerLite](https://ikiform.com/id/guides/newsletter-signup/mailerlite.md), dan [beehiiv](https://ikiform.com/id/guides/newsletter-signup/beehiiv.md). Tanpa kode sama sekali, arahkan webhook ke [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) atau [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md) dan gunakan action alat email mereka. [Dokumentasi webhook](https://ikiform.com/id/docs/webhooks.md) mendaftar aturan URL-nya.

> Situs statis sering punya deploy preview di domain terpisah. Kiriman dari domain yang tidak ada di **Allowed domains** ditandai sebagai spam atau ditolak, jadi tambahkan domain preview selama Anda menguji, atau uji di domain live dan `localhost`.

## Jika situs Anda mengirim Content Security Policy

Banyak host statis memungkinkan Anda mengatur header dari file, seperti `_headers` di Netlify dan Cloudflare Pages. Jika situs Anda mengirim Content-Security-Policy, browser memblokir formulir kecuali kebijakannya mengizinkan Ikiform. `form-action` mencakup kiriman biasa dan `connect-src` mencakup `fetch` milik script:

`_headers`:

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

Kiriman yang diblokir muncul sebagai error CSP di konsol browser dan tidak pernah sampai ke Ikiform, jadi tidak ada yang muncul di dashboard juga.

## Pastikan berfungsi

1. Tambahkan domain live Anda dan `localhost` di **Allowed domains**.
2. Build situs dan buka halaman artikel. Kedua formulir ada di sana, masing-masing dengan nilai `source` sendiri di source halaman.
3. Berlangganan dari formulir artikel dengan alamat milik Anda. Kedua formulir berubah menjadi baris periksa-inbox.
4. Matikan JavaScript dan berlangganan dari footer. Anda mendarat di `/subscribed/`.
5. Buka **Submissions** di dashboard. Kedua pendaftaran menampilkan `post` dan `footer` sebagai `source`-nya.
6. Cari API key alat email Anda di situs hasil build. Key itu tidak ada di sana.

## Hal yang sering menjebak

| Masalah | Yang harus dilakukan |
| --- | --- |
| Tidak ada apa-apa di dashboard, dan ada error CSP di konsol | Tambahkan `https://ikiform.com` ke `form-action` dan `connect-src` di Content-Security-Policy Anda. |
| `422` dengan `unknown_field` untuk `source` | Tambahkan `source` ke definisi field formulir, atau hapus input tersembunyinya. |
| `403 origin_not_allowed`, atau setiap kiriman masuk ke spam | Domain halaman tidak ada di **Allowed domains** formulir. Tambahkan, ditambah `localhost` untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan. |
| Browser mendarat di halaman terima kasih Ikiform, bukan halaman Anda | `_redirect` diabaikan kecuali domainnya ada di **Allowed domains** dan berupa URL `https:`. Tambahkan domainnya, atau atur **Redirect after submit** di pengaturan formulir sebagai gantinya. |

## Pertanyaan

### Bisakah saya menambahkan formulir pendaftaran newsletter ke situs statis tanpa backend?

Bisa. Formulir mengirim dari browser ke backend formulir seperti Ikiform, yang menyimpan dan menyaring setiap pendaftaran. Relay atau alat otomasi yang di-host di tempat lain meneruskannya ke alat email Anda, jadi situs statisnya sendiri tidak menjalankan kode server.

### Bisakah saya memanggil API alat email langsung dari halaman statis?

Jangan. Key apa pun di JavaScript halaman terlihat oleh setiap pengunjung, dan key alat email biasanya bisa membaca atau mengubah seluruh daftar. Simpan key di relay yang berjalan di luar situs.

### Apakah ini bekerja di GitHub Pages?

Ya. Formulirnya HTML biasa dan mengirim ke Ikiform. GitHub Pages tidak bisa menjalankan relay, jadi host relay di Cloudflare Worker dengan domain kustom, atau gunakan alat otomasi seperti Zapier atau n8n.

### Siapa yang mengirim email konfirmasi?

Alat email Anda. Ikiform tidak pernah mengirim email ke orang yang mendaftar. Relay menambahkan pembaca sebagai belum terkonfirmasi dan alat itu mengirim tautan konfirmasinya sendiri.

## Panduan lainnya

- [Formulir pendaftaran newsletter](https://ikiform.com/id/guides/newsletter-signup.md): Buat formulir pendaftaran newsletter dengan markup Anda sendiri, catat persetujuan, blokir pendaftaran bot, dan teruskan setiap subscriber ke Kit, MailerLite, beehiiv, atau alat apa pun.
- [Double opt-in](https://ikiform.com/id/guides/newsletter-signup/double-opt-in.md): Ikiform tidak mengirim email konfirmasi, jadi double opt-in berjalan di alat email Anda: relay menambahkan setiap pendaftaran sebagai belum terkonfirmasi dan alat itu mengirim tautannya.
- [Kit (ConvertKit)](https://ikiform.com/id/guides/newsletter-signup/kit.md): Hubungkan formulir newsletter Anda sendiri ke Kit dengan relay yang membuat subscriber, menambahkannya ke formulir Kit untuk incentive email, dan memberi tag penempatannya.
- [beehiiv](https://ikiform.com/id/guides/newsletter-signup/beehiiv.md): Hubungkan formulir pendaftaran Anda sendiri ke beehiiv: relay membuat setiap subscription dengan double opt-in aktif, penempatan sebagai UTM source, dan halaman sebagai referrer.
- [MailerLite](https://ikiform.com/id/guides/newsletter-signup/mailerlite.md): Hubungkan formulir pendaftaran Anda sendiri ke MailerLite: relay menambahkan pembaca baru sebagai belum terkonfirmasi untuk double opt-in dan tidak pernah meminta subscriber mengonfirmasi dua kali.
- [Formulir newsletter Next.js](https://ikiform.com/id/guides/newsletter-signup/nextjs.md): Formulir newsletter di footer Next.js: server action dengan pemeriksaan persetujuan dan Turnstile, mengirim ke Ikiform, plus route handler yang menambahkan pembaca ke Kit.
