# Menambahkan formulir kontak di Netlify tanpa Netlify Forms

> Jalankan formulir kontak di situs Netlify tanpa Netlify Forms: tanpa deteksi saat build, kiriman JSON dari React, dan formulir terpisah untuk deploy preview.

Terbit 2026-10-25 · https://ikiform.com/id/guides/contact-form-static-site/netlify

Jangan pasang `data-netlify` di formulir dan arahkan `action`-nya ke endpoint Ikiform Anda; Netlify lalu menyajikan halaman tanpa menyentuh kirimannya. Formulir React dan Vue tidak butuh salinan HTML tersembunyi, dan bisa mengirim JSON. Atur ID formulir berbeda untuk deploy preview di `netlify.toml`, karena host preview tidak cocok dengan situs Anda di **Allowed domains**.

## Apa yang berbeda di Netlify

Netlify adalah satu-satunya host statis besar yang punya fitur formulir bawaan. Netlify Forms membaca HTML yang di-deploy setelah setiap build, mencari formulir bertanda `data-netlify` atau `netlify`, dan menerima kiriman ke situs Anda sendiri untuk formulir tersebut. Setiap kiriman lain ke halaman statis gagal, dan di Netlify gagalnya dengan `404`, bukan `405`:

`Terminal`:

```bash
curl -i -X POST -d "message=hello" https://your-site.netlify.app/contact/
```

`Respons`:

```http
HTTP/2 404
server: Netlify
cache-status: "Netlify Edge"; fwd=method; fwd-status=404
content-length: 0
```

`404` itu membuat orang mencari halaman yang hilang padahal tidak hilang. Artinya tidak ada formulir terdeteksi yang cocok dengan kiriman. Perlu tidaknya memakai Netlify Forms adalah pertanyaan terpisah, dan [perbandingan Ikiform vs Netlify Forms](https://ikiform.com/alternatives/netlify-forms) menjawabnya apa adanya: jika situs Anda akan selalu di Netlify dan formulirnya HTML biasa, Netlify Forms lebih sedikit kerjanya. Halaman ini untuk kasus lainnya.

| Situasi Anda | Mengapa Netlify Forms jadi penghalang |
| --- | --- |
| Formulir dirender oleh React, Vue, atau Svelte | Build mem-parsing HTML statis, jadi formulir yang dirender di klien butuh duplikat tersembunyi agar ditemukan Netlify |
| Anda ingin mengirim JSON dari `fetch` | Netlify Forms hanya menerima body urlencoded dan multipart |
| Situs mungkin pindah ke host lain | Formulir mengirim ke domain Anda sendiri, jadi hanya berfungsi selama Netlify yang menyajikan domain itu |
| Pesan harus sampai ke webhook yang Anda verifikasi | Ikiform menandatangani setiap pengiriman dengan header Standard Webhooks dan mencobanya ulang |

Diagram: Netlify Forms dan Ikiform berdampingan di situs Netlify yang sama

1. Dengan Netlify Forms, Netlify memindai HTML setiap deploy untuk mencari formulir bertanda data-netlify.
2. Formulir yang ditemukannya menerima kiriman ke situs Anda sendiri, dan Netlify menyimpannya.
3. Formulir yang tidak ditemukan, seperti yang dirender JavaScript tanpa salinan tersembunyi, mendapat 404 di setiap kiriman.
4. Dengan Ikiform, tidak ada yang perlu ditemukan saat build.
5. Formulir mengirim langsung ke ikiform.com, sebagai HTML atau JSON, lalu Ikiform menyimpannya dan mengirimi Anda email.

Keduanya bisa ada di satu situs. Formulir dengan data-netlify masuk ke Netlify; formulir yang action-nya endpoint Ikiform masuk ke Ikiform.

## Memindahkan formulir dari Netlify Forms

- Hapus `data-netlify` atau `netlify`, `netlify-honeypot`, dan `data-netlify-recaptcha` dari formulir, serta input tersembunyi `form-name`. Jika dibiarkan, `form-name` disimpan sebagai field biasa.
- Hapus salinan statis tersembunyi dari formulir JavaScript, seperti di bawah. Netlify hanya membutuhkannya untuk mendeteksi formulir.
- Atur `action`, atau URL `fetch`, ke endpoint Ikiform Anda. `action="/thanks"` yang lama dipindahkan ke pengaturan **Redirect after submit** formulir.
- Unduh kiriman lama dari halaman **Forms** di Netlify terlebih dahulu. Kiriman itu tidak pindah dengan sendirinya.

`index.html, hapus ini`:

```html
<form name="contact" netlify hidden>
  <input name="name" />
  <input name="email" />
  <textarea name="message"></textarea>
</form>
```

Biarkan **form detection** aktif jika formulir lain di situs masih memakai Netlify Forms. Dokumentasi Netlify menyebutkan mematikan deteksi hanya ditujukan untuk situs yang tidak memakai Netlify Forms. Setelah tidak ada yang memakainya, mematikannya menghemat satu pemindaian di setiap deploy.

### Pengganti pengaturan spam Netlify

Netlify menjalankan Akismet pada kiriman dan menyediakan `netlify-honeypot` serta reCAPTCHA. Di Ikiform, honeypot tetap berfungsi jika Anda mengatur **Honeypot field** formulir ke nama yang sudah Anda pakai, seperti `bot-field`. Skor spam berjalan di setiap kiriman tanpa setup, dan **Spam check** yang diatur ke **Ikiform** menggantikan reCAPTCHA dengan Cloudflare Turnstile tanpa key. Hapus `div` reCAPTCHA saat Anda beralih.

## Formulir React yang mengirim JSON

Tanpa Netlify Forms yang menghalangi, formulir yang dirender di klien tidak butuh duplikat, dan bisa mengirim JSON, yang menjaga tipe data tetap utuh dan memungkinkan Anda merapikan nilai terlebih dahulu. Contoh ini juga mencatat context Netlify mana yang membangun halaman:

`src/ContactForm.tsx`:

```tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = `https://ikiform.com/f/${import.meta.env.VITE_IKIFORM_FORM_ID}`;

interface Message {
  name: string;
  email: string;
  message: string;
  context: string;
}

export function ContactForm() {
  const [sent, setSent] = useState(false);
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const body: Message & Record<string, string> = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      context: import.meta.env.VITE_CONTEXT ?? "local",
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
    setError("");
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    }).catch(() => null);
    const result = await response?.json().catch(() => null);
    if (result?.ok) return setSent(true);
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    setError(Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.");
  }

  if (sent) return <p role="status">Got it. I'll answer from my own inbox, usually the same day.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} minLength={10} required />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button>Send</button>
    </form>
  );
}
```

- `Content-Type: application/json` memberi tahu Ikiform untuk mem-parsing JSON; `Accept: application/json` meminta jawaban JSON.
- `VITE_CONTEXT` adalah nama contoh. Netlify mengatur `CONTEXT` saat build; ekspos ke Vite di perintah build Anda, atau hapus field-nya.
- Honeypot dikirim sebagai key JSON. Tetap sertakan di body meskipun inputnya tersembunyi, agar bot yang mengisinya tetap menandai dirinya sendiri.

## Deploy preview punya formulir sendiri

Deploy preview berada di host seperti `deploy-preview-12--your-site.netlify.app`. Itu bukan subdomain dari `your-site.netlify.app`, jadi tidak cocok di **Allowed domains**, dan kiriman preview masuk ke spam. Beri preview dan branch deploy formulir sendiri di `netlify.toml`, dan atur header Content-Security-Policy di file yang sama:

`netlify.toml`:

```toml
[build]
  command = "npm run build"
  publish = "dist"

[context.production.environment]
  VITE_IKIFORM_FORM_ID = "frm_xxxxxxxxxxxxxxxx"

[context.deploy-preview.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[context.branch-deploy.environment]
  VITE_IKIFORM_FORM_ID = "frm_yyyyyyyyyyyyyyyy"

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com"
```

Biarkan **Allowed domains** formulir preview kosong, atau tambahkan host preview persisnya selama pengujian. Formulir production mendapat domain Anda dan `your-site.netlify.app`. Header di `netlify.toml` berlaku untuk setiap deploy context; jika Anda lebih suka file `_headers`, file itu harus berada di publish directory.

> Jangan pernah menambahkan `netlify.app` begitu saja ke **Allowed domains**. Setiap situs Netlify akan cocok, termasuk salinan formulir Anda di situs orang lain.

## Pastikan berfungsi

1. Buka deploy preview, kirim, dan pastikan pesannya masuk ke formulir preview.
2. Merge, lalu kirim di production. Pastikan pesannya masuk ke formulir production dan halaman **Forms** di Netlify tidak menampilkan yang baru.
3. Cek inbox setiap penerima notifikasi. Masing-masing mendapat email berisi field, dan **Reply** terkirim ke pengirim formulir jika formulir punya field `email`.
4. Kirim dengan honeypot terisi, `_gotcha=bot`. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook.
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

| Masalah | Yang harus dilakukan |
| --- | --- |
| `404` saat mengirim | Kiriman masuk ke situs Anda sendiri. Build yang ter-cache mungkin masih punya `action` lama, atau panggilan `fetch` masih mengirim ke `/`. Cari `form-name` di publish directory. |
| Kiriman masih muncul di Netlify | Ada formulir di halaman yang masih mengirim ke situs Anda sendiri dengan `data-netlify` atau `netlify`, jadi Netlify terus mendeteksi dan menyimpannya. Hapus atributnya, atur `action` ke Ikiform, lalu deploy ulang. |
| `404 not_found` dari Ikiform hanya di preview | Context deploy preview tidak punya `VITE_IKIFORM_FORM_ID`, jadi URL-nya diakhiri `undefined`. Tambahkan di bawah `[context.deploy-preview.environment]`. |
| `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. |
| `422 files_not_supported` | Formulir punya `<input type="file">` dengan file yang dipilih. Upload file belum didukung; hapus input itu atau minta link sebagai gantinya. |

## Pertanyaan

### Bisakah saya memakai form backend lain sementara situs tetap di Netlify?

Bisa. Netlify hanya menangani formulir yang dideteksinya saat build. Formulir tanpa data-netlify yang action-nya mengarah ke endpoint lain disajikan Netlify seperti halaman biasa dan mengirim langsung ke endpoint itu.

### Mengapa formulir saya mendapat 404 di Netlify?

Netlify menjawab POST ke halaman statis dengan 404 kecuali Netlify Forms mendeteksi formulir itu di HTML yang di-deploy. Tandai formulir untuk Netlify Forms atau arahkan action-nya ke backend lain.

### Bisakah saya tetap memakai Netlify Forms untuk satu formulir dan Ikiform untuk yang lain?

Bisa. Deteksi berlaku per formulir, berdasarkan atribut. Pertahankan data-netlify di formulir yang harus ditangani Netlify dan biarkan form detection aktif.

### Apakah kiriman lama di Netlify hilang saat saya beralih?

Kiriman itu tetap di Netlify, bukan di Ikiform. Unduh dari halaman Forms situs sebelum Anda mematikan form detection, dan simpan file-nya bersama arsip Anda.

### Bisakah formulir menerima upload file?

Belum. Kiriman yang berisi file ditolak dengan 422 files_not_supported. Minta link ke file sebagai gantinya, seperti URL shared drive.

## Panduan lainnya

- [Formulir kontak di situs statis](https://ikiform.com/id/guides/contact-form-static-site.md): 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.
- [Vercel](https://ikiform.com/id/guides/contact-form-static-site/vercel.md): Halaman statis di Vercel menjawab POST formulir dengan 405 Method Not Allowed; ini penyebabnya, dan cara mengirim formulir ke backend.
- [Cloudflare Pages](https://ikiform.com/id/guides/contact-form-static-site/cloudflare-pages.md): Tambahkan formulir kontak ke Cloudflare Pages tanpa menulis Function: kirim ke form backend, atur header di _headers, dan pakai key Turnstile Anda sendiri.
- [GitHub Pages](https://ikiform.com/id/guides/contact-form-static-site/github-pages.md): GitHub Pages tidak bisa menjalankan kode server, jadi POST formulir mendapat 405 Not Allowed; kirim ke form backend dan atur redirect project site dengan benar.
- [Amazon S3](https://ikiform.com/id/guides/contact-form-static-site/aws-s3.md): Endpoint website S3 hanya menerima GET dan HEAD lewat HTTP biasa, jadi kirim formulir kontak ke form backend dan atur redirect dengan benar lewat CloudFront.
- [Website portofolio](https://ikiform.com/id/guides/contact-form-static-site/portfolio-website.md): Formulir kontak portofolio yang menanyakan hal yang biasa Anda tanyakan di panggilan pertama: proyek, budget, waktu, dan link, tanpa server dan dengan balasan dari inbox Anda.
