# Cara menghentikan spam di formulir kontak situs statis

> Hentikan spam di formulir situs statis meskipun situs itu tidak bisa menyimpan secret atau menghitung kiriman, dengan partial honeypot, skrip yang aman, dan pemeriksaan sisi server.

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

Situs statis tidak punya server, jadi tidak ada apa pun di halaman yang bisa menyimpan secret key, memverifikasi token captcha, atau menghitung kiriman per pengunjung. Pemeriksaan itu harus berjalan di tempat lain: function di hosting Anda, atau form backend tujuan kiriman formulir. Apa pun pilihannya, pasang honeypot yang aksesibel di formulir, dan pastikan formulir tetap berfungsi tanpa JavaScript.

## Apa yang berbeda di situs statis

Di situs statis, setiap byte yang Anda deploy bersifat publik. Apa pun di HTML atau JavaScript Anda bisa dibaca dan dilewati bot, jadi pemeriksaan spam di sisi client hanya saran, bukan penegakan. Tiga pemeriksaan yang paling penting semuanya butuh server:

- **Memverifikasi token captcha** butuh secret key, yang tidak bisa disimpan di file publik.
- **Menghitung kiriman per pengunjung** butuh penyimpanan yang bertahan di antara request.
- **Membaca honeypot** butuh kode yang berjalan setelah kiriman tiba, di luar jangkauan bot.

Hosting statis juga tidak menerima kiriman ke halaman Anda. Vercel dan GitHub Pages menjawab `POST` formulir ke halaman statis dengan `405 Method Not Allowed`, jadi formulir butuh endpoint bahkan sebelum soal spam muncul.

Diagram: Tiga cara menghubungkan formulir situs statis, dan di mana pemeriksaan spam berjalan di masing-masing

1. Halaman statis yang mengirim ke dirinya sendiri sampai ke hosting yang tidak menjalankan kode, sehingga kiriman gagal dengan 405 atau hilang begitu saja.
2. Dengan function di hosting, halaman mengirim ke function yang menyimpan secret, membaca honeypot, dan memverifikasi token, lalu meneruskan pesannya.
3. Dengan form backend, halaman mengirim langsung ke backend, yang menjalankan honeypot, skor, rate limit, dan pemeriksaan manusia sebelum mengirimi Anda email.

Di kedua baris yang berfungsi, pemeriksaan berjalan setelah kiriman meninggalkan browser. Hanya di situlah bot tidak bisa melewatinya.

## Pilihan Anda

| Opsi | Pemeriksaan spam | Kekurangan |
| --- | --- | --- |
| Netlify Forms | Akismet di setiap kiriman, ditambah field `netlify-honeypot` dan reCAPTCHA v2 jika Anda menambahkannya | Hanya di Netlify. Pindah hosting berarti membangun ulang formulir. Lihat [alternatif Netlify Forms](https://ikiform.com/alternatives/netlify-forms) |
| Function di hosting Anda | Apa pun yang Anda tulis: honeypot, pemeriksaan token, rate limit dengan penyimpanan | Anda yang menulis dan memeliharanya, dan butuh layanan email atau penyimpanan di belakangnya |
| Form backend, seperti Ikiform | Honeypot, skor, rate limit, domain yang diizinkan, dan pemeriksaan manusia, tanpa kode | Layanan tambahan, dengan harga per kiriman |

## Formulir sebagai partial yang bisa dipakai ulang

Endpoint mana pun yang Anda pilih, tulis formulir sekali sebagai partial atau include, agar setiap halaman memakai honeypot yang sama. Versi di bawah mengirim ke Ikiform. Untuk memakai function, ubah `action` ke path function dan hapus `_redirect`.

**Hugo** (`layouts/partials/contact-form.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ "thanks/" | absURL }}" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

**Jekyll** (`_includes/contact-form.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ '/thanks/' | absolute_url }}" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

**Eleventy** (`_includes/contact-form.njk`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" class="contact">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>
  <input type="hidden" name="_redirect" value="{{ site.url }}/thanks/" />
  <p role="status"></p>
  <button>Send message</button>
</form>
```

`assets/contact.css`:

```html
<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>
```

- Honeypot diletakkan di luar layar, di luar urutan tab, dan disembunyikan dari pembaca layar. [Panduan honeypot](https://ikiform.com/id/guides/spam-protection/honeypot.md) menjelaskan setiap atributnya.
- `_redirect` mengarahkan orang ke halaman terima kasih Anda. Ikiform hanya mengikutinya untuk alamat `https:` di salah satu **Allowed domains** formulir.
- Panduan [Hugo](https://ikiform.com/id/docs/guides/hugo.md), [Jekyll](https://ikiform.com/id/docs/guides/jekyll.md), dan [Eleventy](https://ikiform.com/id/docs/guides/eleventy.md) membahas setup, shortcode, dan GitHub Pages.

## Tingkatkan tanpa merusaknya

Skrip bisa membuat pengunjung tetap di halaman, tetapi tidak boleh menjadi satu-satunya cara formulir berfungsi. Skrip ini mengirim dengan `fetch` dan kembali ke kiriman browser biasa dalam dua kasus: saat panggilan jaringan gagal, dan saat pemeriksaan manusia Ikiform meminta token dengan `challenge_required`. Kiriman biasa mendapat halaman pemeriksaan Ikiform, jadi Anda bisa menyalakan pemeriksaan tanpa mengubah skrip.

`contact-form.html`:

```html
<script>
  document.querySelectorAll("form.contact").forEach((form) => {
    const status = form.querySelector("[role=status]");
    const button = form.querySelector("button");

    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      button.disabled = true;
      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) {
          form.replaceChildren("Thanks. We'll reply by email.");
          return;
        }
        if (result.error.code === "challenge_required") {
          form.submit();
          return;
        }
        const wait = response.headers.get("Retry-After");
        if (response.status === 429 && wait) {
          status.textContent = `Too many messages. Try again in ${wait} seconds.`;
        } else {
          status.textContent = result.error.message;
        }
      } catch {
        form.submit();
        return;
      }
      button.disabled = false;
    });
  });
</script>
```

- `form.submit()` mengirim formulir tanpa memicu event `submit` lagi, sehingga fallback tidak bisa berulang tanpa henti.
- `429` menampilkan berapa lama harus menunggu dari `Retry-After`, yang dibuka Ikiform untuk kode browser.
- Kalimat sukses menggantikan formulir, sehingga tidak ada yang mengirimnya dua kali tanpa sengaja.

## Jika Anda menulis function di hosting

Netlify, Cloudflare Pages, dan Vercel semuanya menjalankan function kecil di samping file statis. Function adalah backend Anda sendiri, jadi ia yang memegang pemeriksaan spam. Netlify Function ini membaca honeypot, membuang pesan yang penuh link, memverifikasi token Turnstile, dan mengirimi Anda email lewat Resend. Setiap kiriman yang tertangkap mendapat redirect terima kasih yang sama dengan kiriman asli.

`netlify/functions/contact.mts`:

```ts
import type { Config, Context } from "@netlify/functions";

const LINK = /https?:\/\/|www\./gi;

export default async (req: Request, context: Context) => {
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
  const form = await req.formData();
  const email = String(form.get("email") ?? "");
  const message = String(form.get("message") ?? "");
  const thanks = Response.redirect(new URL("/thanks/", req.url), 303);

  if (String(form.get("_gotcha") ?? "").trim()) return thanks;
  if ((message.match(LINK)?.length ?? 0) >= 3) return thanks;

  const check = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
    method: "POST",
    body: new URLSearchParams({
      secret: process.env.TURNSTILE_SECRET_KEY ?? "",
      response: String(form.get("cf-turnstile-response") ?? ""),
      remoteip: context.ip,
    }),
  });
  const verdict = (await check.json()) as { success: boolean };
  if (!verdict.success) return Response.redirect(new URL("/contact/?error=check", req.url), 303);

  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      from: "Contact form <form@example.com>",
      to: "you@example.com",
      reply_to: email,
      subject: "New message from the website",
      text: message,
    }),
  });
  return thanks;
};

export const config: Config = { path: "/api/contact" };
```

- `context.ip` adalah IP pengunjung, yang bisa dipakai siteverify sebagai `remoteip`.
- Atur `TURNSTILE_SECRET_KEY` dan `RESEND_API_KEY` di environment variable situs, jangan pernah di repository.
- Function ini membuang kiriman yang tertangkap alih-alih menyimpannya, jadi pesan asli yang salah tertangkap akan hilang. Setidaknya catat di log.
- Function ini tidak punya rate limit, karena function tidak punya memori di antara pemanggilan. [Panduan rate limiting](https://ikiform.com/id/guides/spam-protection/rate-limiting.md) menambahkannya dengan Redis.

## Yang dilakukan Ikiform untuk formulir statis

Dengan formulir yang mengirim langsung ke Ikiform, pekerjaan sisi server terjadi di sana. Honeypot-nya `_gotcha`, skor konten dan rate limit per pengunjung berjalan di setiap kiriman, dan **Allowed domains** menandai kiriman dari situs lain sebagai spam. Atur **Spam check** ke **Ikiform** untuk Cloudflare Turnstile tanpa key: kiriman HTML biasa menjalankannya di halaman pemeriksaan, dan skrip di atas kembali ke halaman itu. Kiriman yang dinilai spam mendapat jawaban sukses yang sama dengan kiriman asli, jadi bot tidak belajar apa-apa. Kiriman itu disimpan dengan status `spam`, tidak mengirim email notifikasi maupun webhook, tidak dihitung dalam pemakaian Anda, dan dihapus setelah 30 hari. Setiap Senin, workspace yang memblokir spam mendapat satu email ringkasan berisi total, formulir tersibuk, dan alasan teratas.

> Daftarkan setiap host tempat formulir dipublikasikan di **Allowed domains**: domain Anda, ditambah alamat host itu sendiri selama pengujian, seperti `username.github.io`. Deploy preview Netlify dan URL preview Vercel bukan subdomain dari alamat situs utama Anda; tambahkan host preview yang persis selama Anda mengujinya.

## Pastikan berfungsi

1. Matikan JavaScript dan kirim. Formulir tetap terkirim dan Anda mendarat di halaman terima kasih.
2. Nyalakan JavaScript dan kirim. Formulir digantikan oleh kalimat terima kasih.
3. Kirim dengan honeypot terisi, `_gotcha=bot`. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook.
4. Atur **Spam check** ke **Ikiform** dan kirim dengan skrip aktif. Halaman kembali ke kiriman biasa dan menampilkan halaman pemeriksaan.
5. Tidak ada yang masuk? Ubah filter status di dashboard ke **Spam**. Spam mendapat jawaban sukses yang sama dengan kiriman asli.

## Hal yang sering menjebak

- **`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.
- **HTML yang di-minify.** Beberapa langkah build menghapus atribut yang dianggap kosong atau tidak terpakai. Pastikan halaman hasil build masih punya `aria-hidden` dan `tabindex` pada honeypot.
- **Halaman terima kasih yang di-cache.** Jika halaman terima kasih memakai URL yang sama untuk spam dan kiriman asli, dan memang seharusnya begitu, jangan pernah menaruh apa pun di sana yang bergantung pada keaslian kiriman.
- **Key di repository.** Secret yang di-commit ke repository situs statis publik ikut menjadi publik. Rotasi secret itu jika pernah terjadi.

## Pertanyaan

### Bisakah saya menambahkan captcha ke situs statis?

Anda bisa menampilkan widget-nya, tetapi tokennya harus diverifikasi dengan secret key di server. Pakai function di hosting Anda, form backend yang memverifikasi key Anda, atau pemeriksaan terkelola Ikiform, yang tidak butuh key.

### Apakah honeypot cukup untuk situs statis?

Honeypot menghentikan bot yang paling umum, tetapi hanya jika ada sesuatu di server yang membacanya. Pasangkan dengan rate limit dan skor konten, yang juga butuh server atau form backend.

### Mengapa formulir statis saya menampilkan 405 Method Not Allowed?

Formulir mengirim ke halaman di hosting yang hanya menyajikan file. Arahkan action formulir ke function atau endpoint form backend sebagai gantinya.

### Apakah formulir berfungsi jika JavaScript mati?

Seharusnya ya. Kirim dengan formulir HTML biasa terlebih dulu, lalu tambahkan skrip yang menyempurnakannya dan kembali ke kiriman biasa saat terjadi masalah.

## Panduan lainnya

- [Hentikan spam formulir kontak](https://ikiform.com/id/guides/spam-protection.md): Hentikan spam formulir kontak dengan lapisan yang tidak membebani orang sungguhan: honeypot, waktu pengisian, skor konten, rate limit, dan pemeriksaan manusia hanya bila perlu.
- [Cloudflare Turnstile](https://ikiform.com/id/guides/spam-protection/cloudflare-turnstile.md): Tambahkan Cloudflare Turnstile ke formulir HTML biasa: widget, siteverify di sisi server, key uji, dan cara menjalankannya tanpa key atau server sendiri.
- [Field honeypot](https://ikiform.com/id/guides/spam-protection/honeypot.md): Tambahkan honeypot ke formulir apa pun yang menangkap bot tanpa mengganggu screen reader, keyboard, atau autofill, dengan pemeriksaan server untuk setiap stack.
- [Rate limiting](https://ikiform.com/id/guides/spam-protection/rate-limiting.md): Batasi rate kiriman formulir per pengunjung dengan store bersama, jawab 429 dengan Retry-After, dan hindari mengunci kantor yang berbagi satu IP.
- [Tanpa captcha](https://ikiform.com/id/guides/spam-protection/without-captcha.md): Hentikan sebagian besar spam formulir tanpa captcha sama sekali: honeypot, waktu render bertanda tangan, skor konten, dan rate limit, digabungkan alih-alih dipakai sendiri-sendiri.
- [Alternatif reCAPTCHA](https://ikiform.com/id/guides/spam-protection/recaptcha-alternatives.md): Bandingkan alternatif reCAPTCHA untuk formulir, dari Turnstile dan hCaptcha hingga proof-of-work dan tanpa captcha sama sekali, lalu beralih dengan perubahan kecil yang aman.
