Panduan

Panduan13 menit baca

Menambahkan formulir kontak ke situs statis tanpa backend

Pasang formulir kontak yang berfungsi di situs statis tanpa server: ke mana POST dikirim, spam, balasan lewat email, dan kode untuk setiap host dan framework.

Preet SutharPendiri, Ikiform

Host statis menyajikan file dan menolak POST formulir, biasanya dengan 405 atau 404, jadi formulir harus mengirim ke tempat lain. Arahkan action formulir ke form backend seperti Ikiform, yang menyimpan setiap pesan, menyaring spam, dan mengirimi Anda email dengan pengirim sebagai alamat reply-to. Situs Anda tetap statis: tanpa kode server, tanpa secret key di halaman, dan tanpa perubahan cara deploy.

Situs statis adalah folder berisi HTML, CSS, dan JavaScript yang dibagikan host sebagai file. Itulah sebabnya ia cepat, murah, dan sulit rusak. Itu juga sebabnya formulir kontak menjadi hal pertama yang berhenti berfungsi: kiriman formulir adalah request yang harus diterima oleh sesuatu, dan host statis tidak punya apa pun yang mendengarkannya.

Mengapa formulir gagal di host statis

<form method="post"> tanpa action akan mengirim ke halaman tempat ia berada. Di host statis, halaman itu berupa file, dan file hanya menjawab GET dan HEAD. Kami mengirim POST ke halaman statis live di setiap host besar pada 2026-10-25. Tidak satu pun menyimpan apa-apa:

HostHasil POST ke halaman statis
GitHub Pages405 Not Allowed, berupa halaman HTML kecil
Vercel405 dengan body kosong
Netlify404, dengan halaman 404 situs jika ada, kecuali Netlify Forms mendeteksi formulir itu saat deploy
Cloudflare Pages405 dengan body kosong
Endpoint website Amazon S3405 Method Not Allowed, kode MethodNotAllowed

Solusinya sama di mana pun: kirim kiriman ke URL yang menerimanya. URL itu bisa berupa kode yang Anda tulis dan host sendiri, fitur dari host Anda, atau form backend. Sisa panduan ini memakai form backend, karena situs tetap statis.

Ke mana kiriman formulir kontak pergi, sebelum dan sesudah diperbaikiaction ke situs Anda sendiriaction ke Ikiformhalaman kontakHTML statishost statissajikan file, jawabGET405 atau 404pesan hilanghalaman kontakHTML sama, action baruIkiformcek, simpan, emailhalaman terimakasihkembali ke situs AndaPOSTPOST303Ke mana kiriman formulir kontak pergi, sebelum dan sesudah diperbaikiaction ke situs Anda sendiriaction ke Ikiformhalaman kontakHTML statishost statissajikan file, jawab GET405 atau 404pesan hilanghalaman kontakHTML sama, action baruIkiformcek, simpan, emailhalaman terima kasihkembali ke situs AndaPOSTPOST303
  1. Tanpa action, atau dengan action yang mengarah ke path di situs Anda sendiri, browser mengirim ke host statis.
  2. Host tidak punya kode untuk dijalankan saat menerima POST, jadi ia menjawab 405 atau 404 dan pesannya hilang.
  3. Dengan action yang mengarah ke endpoint Ikiform Anda, kiriman masuk ke server yang menerimanya.
  4. Ikiform memeriksa spam pada pesan, menyimpannya, dan mengirimi Anda email.
  5. Ikiform menjawab dengan redirect 303, dan browser memuat halaman terima kasih di situs Anda sendiri.
Hanya action formulir yang berubah. Halaman, host, dan deploy tetap seperti semula.

Apa yang terjadi pada setiap pesan

Ikiform adalah endpoint tujuan kiriman formulir. Ikiform tidak meng-host halaman Anda atau merender formulir Anda; keduanya tetap milik Anda. Untuk setiap kiriman, Ikiform menjalankan pemeriksaan spam, menyimpan pesan, dan meneruskannya ke tempat Anda membacanya.

Apa yang dilakukan Ikiform dengan satu pesan formulir kontakformulir kontakdi host statis mana punIkiformcek spam, lalu simpanemail notifikasiReply ke pengirimdashboardcari, filter spam, CSVwebhook bertandatanganSlack, Discord, kode AndakirimApa yang dilakukan Ikiform dengan satu pesan formulir kontakformulirkontakdi host statismana punIkiformcek spam, lalusimpanemailnotifikasiReply ke pengirimdashboardcari, filter spam,CSVwebhookbertanda tanganSlack, Discord,kode Andakirim
  1. Pengunjung mengisi formulir kontak, lalu formulir mengirimnya ke endpoint Ikiform Anda.
  2. Ikiform memeriksa honeypot, rate limit, dan skor spam, lalu menyimpan pesannya.
  3. Setiap penerima notifikasi mendapat email, dengan reply-to diatur ke alamat pengirim.
  4. Pesan muncul di dashboard, tempat Anda bisa mencarinya, memulihkan spam, dan mengekspor CSV.
  5. Jika Anda menambahkan webhook, Ikiform mengirim salinan bertanda tangan ke Slack, Discord, alat otomasi, atau kode Anda sendiri.
Email sudah cukup untuk kebanyakan formulir kontak. Tambahkan webhook jika pesan perlu masuk ke tempat Anda biasa bekerja.

Lima cara menerima formulir dari situs statis

PendekatanUnggul dalamKekurangan
Link mailto: atau action="mailto:..."Tanpa setup. Cukup untuk halaman pribadi dengan sedikit pengunjung.Membuka aplikasi email pengunjung, jika mereka sudah mengaturnya. Tidak ada yang disimpan atau disaring, dan alamat Anda ada di HTML untuk dipanen scraper.
Fitur formulir dari host Anda, seperti Netlify FormsSatu atribut, tanpa akun lain, dan biasanya sudah termasuk dalam paket hosting.Hanya berfungsi di host itu. Pindah host berarti membangun ulang formulir, dan formulir yang dirender JavaScript butuh trik tambahan.
Serverless function yang Anda tulis sendiriKendali penuh atas validasi, penyimpanan, dan email yang Anda kirim.Sekarang Anda mengelola backend: API key email, penyaringan spam, rate limit, percobaan ulang, dan log. Format function di tiap host berbeda.
Embed formulir hosted, seperti Google Forms, Tally, atau TypeformTanpa kode. Cocok untuk formulir panjang atau bertahap.Iframe atau link ke situs lain, dengan tampilan dan skrip mereka di halaman Anda.
Form backend, seperti Ikiform, Formspree, Basin, atau Web3FormsHTML Anda sendiri di host mana pun. Penyimpanan, penyaringan spam, dan email tanpa server.Satu akun dan satu tagihan lagi, dan data formulir disimpan di layanan itu.

Jika Anda di Netlify dan tidak berencana pindah, Netlify Forms sulit dikalahkan soal kemudahan. Jika Anda ingin menulis backend sendiri, function adalah proyek akhir pekan yang menyenangkan. Jika Anda ingin formulir tetap berfungsi saat berganti host atau framework, pakai form backend. Perbandingan menyandingkan berbagai backend.

Formulirnya

Semua contoh di bawah mengirim dari browser, karena deploy statis tidak punya server untuk mengirim. Pilih stack Anda; pilihan itu berlaku untuk setiap contoh kode di panduan ini. Static site generator punya halaman setup sendiri: Jekyll, Hugo, Eleventy, dan Gatsby.

Hanya ini integrasi yang dibutuhkan untuk situs yang ditulis manual. Formulir mengirim dengan cara biasa, jadi tetap berfungsi saat JavaScript mati, dan browser mendarat di thanks.html Anda setelah berhasil terkirim.

contact.html
<form id="contact" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" maxlength="120" required />
  <p class="field-error" data-error-for="name" hidden></p>

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p class="field-error" data-error-for="email" hidden></p>

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" minlength="10" maxlength="5000" required></textarea>
  <p class="field-error" data-error-for="message" hidden></p>

  <input type="hidden" name="_redirect" value="https://example.com/thanks.html" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button type="submit">Send message</button>
  <p id="contact-status" role="status"></p>
</form>
<p id="contact-sent" role="status" hidden>Thanks. Your message is on its way, and I'll reply by email.</p>

<script src="/contact.js" defer></script>

Skripnya opsional. Skrip menjaga pengunjung tetap di halaman, menaruh setiap error field di bawah inputnya, dan membiarkan pesan yang sudah diketik tetap ada saat terjadi kegagalan.

contact.js
const form = document.querySelector("#contact");
const status = document.querySelector("#contact-status");
const done = document.querySelector("#contact-sent");
const button = form.querySelector("button");

function showErrors(fields = {}) {
  for (const slot of form.querySelectorAll("[data-error-for]")) {
    const name = slot.dataset.errorFor;
    const message = fields[name]?.[0] ?? "";
    slot.textContent = message;
    slot.hidden = !message;
    form.elements[name]?.setAttribute("aria-invalid", message ? "true" : "false");
  }
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  button.disabled = true;
  status.textContent = "Sending";
  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.hidden = true;
      done.hidden = false;
      return;
    }
    showErrors(result.error.fields);
    status.textContent = result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
  • Ganti frm_xxxxxxxxxxxxxxxx dengan ID formulir Anda dari dashboard. ID ini memang publik: ia ada di HTML Anda begitu Anda deploy. Jangan pernah menaruh secret API key di situs statis.
  • Pertahankan nama field email sebagai email. Ikiform menjadikannya alamat reply-to email notifikasi dan memeriksanya terhadap domain email sekali pakai.
  • Input tersembunyi _gotcha adalah honeypot. Orang tidak pernah melihatnya; bot yang mengisi setiap input menandai dirinya sendiri sebagai spam.
  • Versi JavaScript mengirim Accept: application/json, sehingga Ikiform menjawab 201 saat berhasil, atau 422 dengan objek error.fields yang menyebutkan setiap field dan masalahnya.

Jika framework Anda memang berjalan di server saat production, Anda bisa mengirim dari sana dan menjauhkan endpoint dari halaman. Panduan Next.js, SvelteKit, dan Nuxt menunjukkan versi server-nya, dan Dari server membahas secret key.

Versi sisi server mengirim dengan secret key, sehingga Ikiform melewati Allowed domains dan pemeriksaan manusianya. Semua kiriman lalu datang dari IP server Anda, dan batas Standard 5 per menit per pengunjung menghitung semua pengunjung Anda sebagai satu. Pilih rate limit Relaxed atau Custom untuk formulir yang Anda kirim lewat server, dan simpan IKIFORM_SECRET_KEY di environment variable khusus server.

Field untuk formulir kontak

Nama, email, dan pesan sudah mencakup hampir semua formulir kontak. Pilihan topik layak ditambahkan jika jenis pesan yang berbeda dijawab oleh orang yang berbeda. Coba formulir di bawah: formulir menjalankan validator Ikiform di browser Anda dan menampilkan apa yang akan disimpan, atau 422 yang akan Anda terima.

Pratinjau

Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.

Definisinya sama dengan template formulir kontak. Tempel di tab Fields formulir dan Ikiform menerapkannya di server, apa pun isi HTML-nya.
FieldAlasanPerhatikan
nameCara Anda menyapa mereka di balasanLink di field nama adalah sinyal spam yang kuat, jadi pertahankan ID name
emailTujuan balasanWajib. type="email" memunculkan keyboard ponsel yang tepat
topicMengarahkan penjualan dan dukungan ke orang yang tepatSelect hanya menerima nilai yang tercantum, jadi aman untuk difilter
messageInti dari formulir iniPanjang minimum 10 mencegah kiriman satu kata

Jangan sertakan nomor telepon, ukuran perusahaan, dan apa pun yang tidak akan Anda pakai di balasan pertama. Setiap field tambahan mengurangi jumlah pesan. Tanpa definisi field, Ikiform menyimpan input bernama apa pun yang dikirim formulir; dengan definisi, Ikiform menolak kiriman yang tidak cocok.

Upload file belum didukung. Kiriman berisi file mendapat 422 files_not_supported.

Membalas dari inbox Anda

Email notifikasi dikirim ke maksimal 10 penerima terkonfirmasi per formulir. Jika formulir punya field email, reply-to email itu adalah pengirimnya, jadi Reply di aplikasi email Anda langsung membalas ke mereka. Anda tidak perlu menyalin alamat dari dashboard.

Notifikasi dibatasi 50 email per formulir per jam, lalu satu ringkasan per jam, sehingga gelombang spam tidak bisa membanjiri inbox Anda. Pesan yang ditandai spam sama sekali tidak mengirim email. _subject tidak mengubah subjek email notifikasi. Nilainya diteruskan ke webhook dan API sebagai meta.subject.

Ikiform tidak mengirim email ke orang yang mengisi formulir. Email notifikasi hanya dikirim ke Anda dan tim, jadi tidak ada autoresponder, email konfirmasi, atau double opt-in. Kirim semua itu dari alat email Anda, dipicu oleh webhook.

Ingin pesan masuk ke chat tim, sebagai pengganti atau tambahan email? Tambahkan webhook dan ikuti panduan Slack, Discord, atau Telegram.

Yang dilihat pengunjung setelah mengirim

Kiriman HTML biasa berakhir dengan redirect. Ikiform menjawab kiriman browser yang berhasil dengan 303 See Other, dan browser memuat tujuannya dengan GET, yang dengan senang hati disajikan host statis Anda.

Tiga request di balik satu kiriman formulir HTML biasabrowserpengunjung menekanKirimikiform.com/f/โ€ฆmenyimpan pesan/thanks.htmlAndafile statisPOST303, lalu GETTiga request di balik satu kiriman formulir HTML biasabrowserpengunjung menekan Kirimikiform.com/f/โ€ฆmenyimpan pesan/thanks.html Andafile statisPOST303, lalu GET
  1. Browser mengirim field formulir ke endpoint Ikiform Anda.
  2. Ikiform menyimpan pesan dan menjawab 303 See Other dengan header Location.
  3. Browser mengikuti redirect dengan GET ke halaman terima kasih Anda, yang disajikan host statis seperti file lainnya.
Tanpa tujuan redirect, langkah 3 memuat halaman terima kasih hosted milik Ikiform, bukan halaman Anda.

Ikiform memilih tujuan dengan urutan ini:

  • Redirect after submit di pengaturan formulir. URL http: atau https: apa pun bisa dipakai, karena hanya tim Anda yang bisa mengaturnya. Ini pilihan paling sederhana untuk satu situs.
  • Field _redirect di formulir, jika berupa URL https: di salah satu Allowed domains formulir. Pakai ini jika satu formulir melayani beberapa situs atau bahasa.
  • Selain itu, halaman terima kasih hosted milik Ikiform, yang menautkan kembali ke situs Anda jika kiriman berasal dari halaman https: yang diizinkan.

Jadikan halaman terima kasih sebagai file sungguhan dan jauhkan dari hasil pencarian. Sebutkan kapan Anda akan membalas; itu mencegah email kedua dari orang yang sama.

thanks.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="robots" content="noindex" />
    <title>Message sent</title>
  </head>
  <body>
    <h1>Thanks, your message is on its way</h1>
    <p>I read every message and reply within two working days, from the address on this site.</p>
    <p><a href="/">Back to the home page</a></p>
  </body>
</html>

Versi JavaScript di atas tidak memakai redirect. Kiriman dengan Accept: application/json mendapat JSON sebagai jawaban, dan halaman menampilkan kalimat sukses sendiri.

Spam di formulir statis

Endpoint Anda ada di source halaman, dan bot membaca source halaman. Bersiaplah menerima spam dalam beberapa hari setelah live. Ikiform menyusun perlindungannya berlapis sehingga Anda bisa mulai tanpa setup apa pun:

PerlindunganYang dihentikanSetup
HoneypotBot yang mengisi setiap inputInput tersembunyi _gotcha
Skor spamLink di nama (+3), banyak link (+2), frasa spam yang dikenal (+2), dan alamat sekali pakai. Pada skor 5 pesan disimpan sebagai spamTidak ada
Rate limitSkrip yang mengirim berulang-ulangAktif secara default: Standard, 5 per menit per pengunjung
Domain yang diizinkanSalinan formulir Anda yang mengirim dari situs lainTambahkan domain Anda di pengaturan formulir
Pemeriksaan manusiaBot yang mengendalikan browser sungguhanAtur Spam check ke Ikiform untuk Cloudflare Turnstile tanpa key

Spam mendapat jawaban sukses yang sama dengan pesan asli, jadi bot tidak belajar apa-apa, dan spam tidak mengirim email atau webhook. Formulir kontak jarang butuh lebih dari satu kiriman dari satu orang dalam beberapa menit, jadi Strict, 2 per 10 menit per pengunjung, cocok setelah Anda live. Dokumentasi spam membahas setiap lapisan, dan pemeriksaan manusia butuh satu panggilan tambahan di versi JavaScript: challenge() dari SDK.

Domain yang diizinkan dan deploy preview

Allowed domains mencocokkan sebuah domain beserta subdomainnya. Host statis memberi Anda lebih banyak domain dari yang Anda kira: domain production, subdomain milik host, dan alamat preview per branch atau pull request. Sebagian preview adalah subdomain dari alamat host situs Anda dan cocok dengannya; sebagian lain tidak dan perlu ditambahkan sendiri.

HostTambahkanPreview
GitHub Pagesusername.github.io dan domain kustom AndaTidak ada preview secara default
Vercelyour-project.vercel.app dan domain AndaURL branch tidak cocok; tambahkan host persisnya selama pengujian
Netlifyyour-site.netlify.app dan domain Andadeploy-preview-12--your-site.netlify.app tidak cocok
Cloudflare Pagesyour-project.pages.dev dan domain Andaabc123.your-project.pages.dev sudah cocok

Jangan pernah menambahkan host bersama begitu saja, seperti github.io, vercel.app, atau netlify.app. Setiap situs di host itu akan cocok, termasuk milik orang lain.

Jika situs Anda mengirim Content-Security-Policy

Banyak template statis menyertakan Content-Security-Policy yang ketat. Dua direktif menentukan apakah formulir berfungsi: form-action untuk kiriman HTML biasa, dan connect-src untuk versi fetch. Jika salah satunya tidak ada, browser memblokir request sebelum keluar dari halaman, dan satu-satunya petunjuk hanya pesan di console.

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

Pertahankan 'self' di form-action: beberapa browser juga memeriksa redirect kembali ke halaman terima kasih Anda terhadapnya. Letak header tergantung host: _headers di Netlify dan Cloudflare Pages, vercel.json di Vercel, tag <meta> di GitHub Pages, dan response headers policy CloudFront di depan S3. Halaman host di bawah menunjukkan masing-masing.

Uji sebelum deploy

Buka halaman lewat server lokal, bukan dengan mengklik dua kali file-nya. Halaman yang dibuka sebagai file:// mengirim Origin: null, yang tidak cocok dengan domain mana pun dan tidak dihitung sebagai uji lokal. Salah satu perintah ini menyajikan folder saat ini:

Terminal
npx serve .

python3 -m http.server 8000

Tambahkan localhost ke Allowed domains selama pengujian. Kiriman dari localhost dihitung sebagai uji: workspace tanpa paket mendapat 50 per bulan, jadi Anda bisa membangun seluruh formulir sebelum membayar.

Pilih situasi Anda

Formulir di atas berfungsi di semua host statis. Setiap host punya cara gagal sendiri, serta domain preview dan file header sendiri. Halaman-halaman ini membahasnya.

Pastikan berfungsi

Jalankan ini sekali di domain live Anda dan sekali di preview, jika host Anda menyediakannya. Ikiform tidak punya paket gratis. Pengujian gratis: workspace tanpa paket menerima 50 kiriman uji per bulan dari localhost. Untuk go live, pilih Pro mulai $6/bln untuk 500 kiriman, atau bayar sekali: 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa. Lihat harga.

  1. Di Allowed domains formulir, tambahkan domain situs Anda, ditambah localhost selama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja.
  2. Kirim dari server lokal Anda dan pastikan pesannya muncul di Submissions.
  3. Deploy, lalu kirim dari situs live dengan JavaScript dimatikan. Anda seharusnya mendarat di halaman terima kasih Anda sendiri.
  4. Nyalakan lagi JavaScript dan kirim dengan pesan kosong. Error seharusnya muncul di halaman, bukan sebagai halaman error hosted.
  5. Cek inbox setiap penerima notifikasi. Masing-masing mendapat email berisi field, dan Reply terkirim ke pengirim formulir jika formulir punya field email.
  6. Kirim dengan honeypot terisi, _gotcha=bot. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook.
  7. Tidak ada yang masuk? Ubah filter status di dashboard ke Spam. Spam mendapat jawaban sukses yang sama dengan kiriman asli.

Pemecahan masalah

MasalahYang harus dilakukan
405 Method Not Allowed atau 404 tepat setelah menekan KirimFormulir mengirim ke situs Anda sendiri. Cek atribut action di HTML yang sudah di-deploy, bukan di source Anda; langkah build atau komponen <Form> dari framework mungkin telah menggantinya.
Tidak terjadi apa-apa, dan console menyebut Content-Security-PolicyTambahkan https://ikiform.com ke form-action untuk kiriman biasa dan ke connect-src untuk fetch.
403 origin_not_allowed, atau setiap kiriman masuk ke spamDomain halaman tidak ada di Allowed domains formulir. Tambahkan, ditambah localhost untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan.
Uji lokal ditolak dengan 402 plan_requiredHalaman dibuka sebagai URL file://, jadi kiriman tidak punya origin yang bisa dipakai dan dihitung sebagai live. Sajikan folder dengan server lokal dan uji dari localhost.
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.
422 validation_failedKiriman tidak cocok dengan definisi field formulir. error.fields menyebutkan setiap field dan alasannya. Samakan name input dengan ID field.
Kiriman ada di dashboard tetapi tidak ada email atau webhook yang datangKiriman itu disimpan sebagai spam. Spam tidak mengirim apa pun. Buka di filter Spam untuk melihat alasannya, dan tandai sebagai bukan spam untuk mengirim email dan webhook-nya.
429 rate_limitedSatu pengunjung mengirim melebihi rate limit formulir. Standard mengizinkan 5 per menit per pengunjung. Tunggu sesuai Retry-After, atau pilih Relaxed atau Custom untuk formulir yang mengirim dari satu IP bersama.

Pertanyaan

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