Hentikan spam formulir kontak

Hentikan spam formulir kontak6 menit baca

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.

Preet SutharPendiri, Ikiform

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.

Tiga cara menghubungkan formulir situs statis, dan di mana pemeriksaan spam berjalan di masing-masingHanya halamanDengan function di hostingDengan form backendhalaman statiskirim ke diri sendirihostingtak ada kode405, atau hilanghalaman statisfunctionsecret, honeypot, cektokenemail ataupenyimpananhalaman statisform backendhoneypot, skor, limit,Turnstileinbox dan webhookTiga cara menghubungkan formulir situs statis, dan di mana pemeriksaan spam berjalan di masing-masingHanya halamanDengan function di hostingDengan form backendhalaman statiskirim ke diri sendirihostingtak ada kode405, atau hilanghalaman statisfunctionsecret, honeypot, cek tokenemail atau penyimpananhalaman statisform backendhoneypot, skor, limit, Turnstileinbox dan webhook
  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

OpsiPemeriksaan spamKekurangan
Netlify FormsAkismet di setiap kiriman, ditambah field netlify-honeypot dan reCAPTCHA v2 jika Anda menambahkannyaHanya di Netlify. Pindah hosting berarti membangun ulang formulir. Lihat alternatif Netlify Forms
Function di hosting AndaApa pun yang Anda tulis: honeypot, pemeriksaan token, rate limit dengan penyimpananAnda yang menulis dan memeliharanya, dan butuh layanan email atau penyimpanan di belakangnya
Form backend, seperti IkiformHoneypot, skor, rate limit, domain yang diizinkan, dan pemeriksaan manusia, tanpa kodeLayanan 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.

<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>
assets/contact.css
<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 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, Jekyll, dan Eleventy 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
<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
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 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

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.