Melampaui formulir builder6 menit baca
Batas Netlify Forms: apa yang terjadi di 100 kiriman, dan jalan keluarnya
Paket Netlify lama menghitung kiriman formulir per situs dan meng-upgrade Anda otomatis, dan paket kredit bisa menjeda situs, jadi pindahkan formulir ke backend.
Di paket lama Netlify, setiap situs mendapat 100 kiriman formulir per bulan, dan melewatinya meng-upgrade situs ke level Forms berbayar secara otomatis. Di paket berbasis kredit untuk akun yang dibuat sejak September 2025, formulir gratis, tetapi tim gratis yang kehabisan kredit akan dijeda semua situsnya, termasuk formulir. Memindahkan formulir ke Ikiform berarti menghapus atribut Netlify dan mengatur action, di HTML biasa atau framework apa pun.
Apa yang berbeda di Netlify
Netlify bukan site builder; ia host dengan formulir bawaan. Itu membentuk batasnya sekaligus jalan keluarnya. Batas mana yang berlaku tergantung kapan akun Anda dibuat:
| Akun | Apa kata dokumentasi Netlify | Apa artinya bagi formulir yang ramai |
|---|---|---|
| Paket lama, dibuat sebelum 4 September 2025 | Forms Level 0 mencakup 100 kiriman dan 10 MB upload per situs per bulan. Melewatinya meng-upgrade situs ke Forms Level 1, dengan 1.000 kiriman, ditagih di akhir siklus penagihan. Setelah itu, paket penggunaan tambahan menambah 500 sekaligus | Gelombang spam bisa memindahkan situs ke level berbayar tanpa ada yang memutuskannya. Di tier gratis lama, mencapai batas menjeda setiap situs di akun |
| Paket berbasis kredit, dibuat sejak saat itu | Kiriman formulir gratis dan tanpa batas, tanpa level | Formulirnya sendiri tidak berbiaya. Tetapi saat tim gratis menghabiskan kredit bulanannya, Netlify menjeda semua proyeknya, dan proyek yang dijeda tidak menerima kiriman formulir |
Dokumentasi Netlify sendiri jelas menyatakan upgrade lama terjadi otomatis dan Anda tidak bisa turun lagi selama penggunaan di atas level yang lebih rendah. Periksa jenis paket tim Anda dan penggunaan formulir situs di dashboard Netlify sebelum memutuskan apa pun.
Alasan kedua orang pindah tidak ada hubungannya dengan angka. Netlify Forms bekerja dengan membaca HTML yang Anda deploy:
- Formulir ditemukan saat deploy. Formulir yang dirender React, Vue, atau Svelte tidak ada di HTML itu, jadi Anda menyimpan salinan statis tersembunyi agar ditemukan build, dan mengirim ke sana dengan field
form-name. - Dengan Next.js, adapter OpenNext butuh salinan itu sebagai file statis di
public/, dan post harus pergi ke file statis itu, bukan ke halaman Next.js. Build gagal jikadata-netlifymuncul di kode React tanpa salinan itu. - Kiriman harus urlencoded atau multipart. JSON tidak diterima.
- Formulir hanya berfungsi selama situs di-deploy di Netlify. Pindah host berarti memindahkan formulir.
- Di paket lama, situs memulai bulan di Forms Level 0 dengan 100 kiriman termasuk.
- Kiriman ke-101 tiba. Dokumentasi penggunaan Netlify menggambarkan penggunaan situs, jadi lonjakan spam bisa menjadi pendorongnya melewati batas.
- Netlify meng-upgrade situs ke Forms Level 1, dengan 1.000 kiriman, dan menagihnya di akhir siklus.
- Setelah Level 1, paket penggunaan tambahan menambah 500 kiriman sekaligus.
- Dengan formulir mengirim ke Ikiform, halaman tetap di Netlify tetapi post pergi ke endpoint Ikiform.
- Netlify Forms tidak pernah menerima post, jadi tidak ada yang perlu dihitung atau di-upgrade.
Pindahkan formulir, per stack
Sebelum mengubah kode apa pun, buat satu formulir Ikiform per nama formulir Netlify dan atur Honeypot field-nya ke bot-field. Dengan begitu input tersembunyi yang sudah ada tetap berfungsi sebagai honeypot, tanpa ganti nama. Pilih stack Anda:
HTML biasa di Netlify butuh perubahan paling sedikit. Hapus atribut Netlify dan input form-name, arahkan action ke endpoint, dan pindahkan path halaman terima kasih ke _redirect sebagai URL lengkap. Input bot-field tetap sebagai honeypot Anda.
<form name="contact" method="POST" action="/thanks" data-netlify="true" netlify-honeypot="bot-field">
<input type="hidden" name="form-name" value="contact" />
<p hidden><input name="bot-field" /></p>
<label>Email <input type="email" name="email" required /></label>
<label>Topic
<select name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
</label>
<label>Message <textarea name="message" required></textarea></label>
<button type="submit">Send</button>
</form>Di Netlify, formulir Next.js hanya berfungsi dengan Netlify Forms lewat salinan statis di public/, seperti public/__forms.html, yang menjadi tujuan kiriman komponen Anda. Hapus file itu, hapus pengaturan NETLIFY_NEXT_VERIFY_FORMS jika ada, dan kirim dari komponen ke Ikiform. Formulir juga bisa mengirim tanpa JavaScript, berkat action-nya.
"use client";
import { type FormEvent, useState } from "react";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
export function ContactForm() {
const [state, setState] = useState<"idle" | "sending" | "sent">("idle");
const [error, setError] = useState("");
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setState("sending");
setError("");
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.currentTarget),
});
const result = await response.json();
if (result.ok) return setState("sent");
setError(result.error.fields?.message?.[0] ?? result.error.fields?.email?.[0] ?? result.error.message);
} catch {
setError("Couldn't send. Check your connection and try again.");
}
setState("idle");
}
if (state === "sent") return <p role="status">Thanks. We read every message and reply within a day.</p>;
return (
<form action={ENDPOINT} method="post" onSubmit={onSubmit}>
<input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="topic">Topic</label>
<select id="topic" name="topic" defaultValue="question">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{error ? <p role="alert">{error}</p> : null}
<button disabled={state === "sending"}>{state === "sending" ? "Sending" : "Send"}</button>
</form>
);
}Halaman Astro statis sama sekali tidak butuh JavaScript. Hapus data-netlify dari formulir, atur endpoint sebagai action, dan susun redirect dari site di astro.config.mjs. Halaman bisa tetap di-prerender.
---
const endpoint = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const thanksUrl = new URL("/thanks", Astro.site).href;
---
<form method="post" action={endpoint}>
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<input type="hidden" name="_redirect" value={thanksUrl} />
<button>Send</button>
</form>Aplikasi TanStack Start di Netlify merender formulir di React, yang tidak bisa dilihat deteksi saat deploy Netlify. Kirim lewat server function, dan buang salinan statis tersembunyi. Atur IKIFORM_SECRET_KEY di environment variable situs di Netlify. Function meneruskan header Origin dari browser, jadi post dari localhost dihitung sebagai uji gratis sebelum workspace Anda punya paket.
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";
const sendMessage = createServerFn({ method: "POST" })
.validator((data: unknown) => {
if (!(data instanceof FormData)) throw new Error("Expected form data");
return {
email: String(data.get("email") ?? "").trim(),
topic: String(data.get("topic") ?? "question"),
message: String(data.get("message") ?? "").trim(),
"bot-field": String(data.get("bot-field") ?? ""),
};
})
.handler(async ({ data }) => {
const origin = getRequestHeader("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify(data),
});
const result = await response.json();
return result.ok ? { sent: true, error: "" } : { sent: false, error: String(result.error.message) };
});
export const Route = createFileRoute("/contact")({ component: Contact });
function Contact() {
const send = useServerFn(sendMessage);
const [state, setState] = useState({ sent: false, error: "" });
const [pending, setPending] = useState(false);
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setPending(true);
try {
setState(await send({ data: new FormData(event.currentTarget) }));
} catch {
setState({ sent: false, error: "Couldn't send. Try again." });
} finally {
setPending(false);
}
}
if (state.sent) return <p role="status">Thanks. We read every message and reply within a day.</p>;
return (
<form method="post" onSubmit={onSubmit}>
<input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="topic">Topic</label>
<select id="topic" name="topic" defaultValue="question">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{state.error ? <p role="alert">{state.error}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Send"}</button>
</form>
);
}Dengan React Router di framework mode, action route meneruskan post. FormData diteruskan tanpa perubahan, jadi bot-field sampai ke pemeriksaan honeypot Ikiform, dan header Origin dari browser ikut terbawa agar post dari localhost dihitung sebagai uji. Hapus salinan statis yang Anda simpan untuk Netlify.
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/contact";
export async function action({ request }: Route.ActionArgs) {
const form = await request.formData();
const origin = request.headers.get("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: form,
});
const result = await response.json();
if (result.ok) return { sent: true, error: "" };
return data({ sent: false, error: String(result.error.message) }, { status: response.status });
}
export default function Contact({ actionData }: Route.ComponentProps) {
const sending = useNavigation().state === "submitting";
if (actionData?.sent) return <p role="status">Thanks. We read every message and reply within a day.</p>;
return (
<Form method="post">
<input name="bot-field" type="text" tabIndex={-1} autoComplete="off" hidden />
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required />
<label htmlFor="topic">Topic</label>
<select id="topic" name="topic" defaultValue="question">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" required />
{actionData?.error ? <p role="alert">{actionData.error}</p> : null}
<button disabled={sending}>{sending ? "Sending" : "Send"}</button>
</Form>
);
}Halaman SvelteKit di Netlify bisa mengirim langsung dari browser, tanpa form action buatan Anda. Atribut action membuat formulir tetap berfungsi sebelum hydration. Hapus salinan formulir yang di-prerender yang Anda tambahkan untuk Netlify.
<script lang="ts">
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
let sent = $state(false);
let sending = $state(false);
let error = $state("");
async function send(event: SubmitEvent) {
event.preventDefault();
sending = true;
error = "";
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.currentTarget as HTMLFormElement),
});
const result = await response.json();
if (result.ok) sent = true;
else error = result.error.message;
} catch {
error = "Couldn't send. Check your connection and try again.";
}
sending = false;
}
</script>
{#if sent}
<p role="status">Thanks. We read every message and reply within a day.</p>
{:else}
<form method="post" action={ENDPOINT} onsubmit={send}>
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
{#if error}<p role="alert">{error}</p>{/if}
<button disabled={sending}>{sending ? "Sending" : "Send"}</button>
</form>
{/if}Di Nuxt komponen mengirim dari browser dengan $fetch, jadi tidak ada server route yang perlu di-deploy. $fetch melempar error pada jawaban non-2xx, dan envelope error Ikiform ada di data milik error itu. Hapus formulir statis tersembunyi dari public/ dan field form-name.
<script setup lang="ts">
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const sent = ref(false);
const sending = ref(false);
const error = ref("");
async function send(event: Event) {
sending.value = true;
error.value = "";
try {
await $fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.target as HTMLFormElement),
});
sent.value = true;
} catch (failure) {
const data = (failure as { data?: { error?: { message?: string } } }).data;
error.value = data?.error?.message ?? "Couldn't send. Check your connection and try again.";
} finally {
sending.value = false;
}
}
</script>
<template>
<p v-if="sent" role="status">Thanks. We read every message and reply within a day.</p>
<form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="sending">{{ sending ? "Sending" : "Send" }}</button>
</form>
</template>SPA React di Netlify mengirim data urlencoded ke / dengan field form-name, dan butuh salinan statis di index.html untuk deteksi. Kini ia mengirim FormData ke endpoint. Hanya request-nya yang berubah:
const body = new URLSearchParams(new FormData(event.currentTarget) as unknown as Record<string, string>);
body.set("form-name", "contact");
await fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});Hapus <form netlify> tersembunyi dari index.html setelah tidak ada lagi yang mengirim ke Netlify.
Post sisi browser yang sama di single-file component Vue. Hapus formulir tersembunyi dari index.html yang memungkinkan Netlify mendeteksinya.
<script setup lang="ts">
import { ref } from "vue";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const state = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");
async function send(event: Event) {
state.value = "sending";
error.value = "";
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(event.target as HTMLFormElement),
});
const result = await response.json();
if (result.ok) {
state.value = "sent";
return;
}
error.value = result.error.message;
} catch {
error.value = "Couldn't send. Check your connection and try again.";
}
state.value = "idle";
}
</script>
<template>
<p v-if="state === 'sent'" role="status">Thanks. We read every message and reply within a day.</p>
<form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
<input name="bot-field" type="text" tabindex="-1" autocomplete="off" hidden />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<label for="topic">Topic</label>
<select id="topic" name="topic">
<option value="question">A question</option>
<option value="bug">Something is broken</option>
<option value="work">Working together</option>
</select>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="state === 'sending'">{{ state === "sending" ? "Sending" : "Send" }}</button>
</form>
</template>IKIFORM_SECRET_KEY di environment variable khusus server.Setiap versi mempertahankan nama field yang sama seperti di Netlify, jadi email notifikasi dan kolom CSV terasa familier. Perbandingan Netlify Forms berisi pemetaan atribut lengkap, termasuk data-netlify-recaptcha dan input file.
Deploy preview dan allowed domains
Netlify memberi setiap pull request alamatnya sendiri, seperti deploy-preview-42--your-site.netlify.app. Itu bukan subdomain dari your-site.netlify.app, jadi mencantumkan alamat situs Anda di Allowed domains tidak mencakup preview, dan post dari preview masuk ke spam dengan origin_mismatch.
- Tambahkan
your-site.netlify.appdan domain kustom Anda untuk produksi. - Tambahkan host preview tertentu selama Anda mengujinya, lalu hapus.
- Jangan tambahkan
netlify.appsaja. Setiap situs di Netlify akan diizinkan mengirim ke formulir Anda. - Atau arahkan preview ke formulir Ikiform kedua tanpa allowed domains, lewat environment variable, dan jaga produksi tetap ketat.
Beralih tanpa kehilangan kiriman
- Unduh kiriman lama setiap formulir sebagai CSV dari bagian Forms situs. Ikiform tidak punya fitur import.
- Deploy formulir baru ke preview dan kirim uji. Periksa nama field di dashboard Ikiform.
- Merge ke produksi dan kirim uji lagi dari domain live.
- Ganti outgoing webhook Netlify dengan webhook Ikiform. Netlify menandatangani dengan token JWS; Ikiform memakai header Standard Webhooks, jadi verifier Anda juga berubah.
- Setelah tidak ada post yang sampai ke Netlify selama beberapa hari, matikan deteksi formulir di pengaturan Forms situs.
422 files_not_supported. Jika formulir butuh file, biarkan formulir itu di Netlify Forms dan pindahkan sisanya.Pastikan berfungsi di Netlify
- Di Allowed domains formulir, tambahkan domain situs Anda, ditambah
localhostselama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja. - Deploy preview, tambahkan host-nya, dan kirim formulir dari sana.
- Buka Submissions di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.
- Pastikan field-nya
email,topic, danmessage, tanpaform-namedan tanpabot-field. - Buka tab Forms situs di Netlify dan pastikan uji tadi tidak ada di sana.
- Kirim sekali dengan
bot-fieldterisi. Kiriman itu disimpan sebagai spam dan tidak mengirim apa pun. - Cek inbox setiap penerima notifikasi. Masing-masing mendapat email berisi field, dan Reply terkirim ke pengirim formulir jika formulir punya field
email.
Hal yang sering menjebak
| Masalah | Yang harus dilakukan |
|---|---|
Field form-name muncul di Ikiform | Input form-name tersembunyi masih ada di formulir, atau kode Anda masih mengaturnya. Hapus; Ikiform mengenali formulir dari URL. |
| Build Next.js gagal dengan menyebut forms | Atribut netlify atau data-netlify masih ada di kode React Anda. Hapus bersama public/__forms.html. |
| Post dari preview masuk ke spam | Host preview tidak ada di Allowed domains. Tambahkan selama Anda menguji, atau gunakan formulir kedua untuk preview. |
| Setiap post ditandai spam, bahkan yang asli | Honeypot field masih _gotcha sementara input Anda bot-field, atau sebaliknya. Samakan keduanya. |
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. |
429 rate_limited | Satu 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
Di paket lama, Netlify meng-upgrade situs ke Forms Level 1, yang mencakup 1.000 kiriman per bulan, dan menagihnya di akhir siklus penagihan. Di tier gratis lama, mencapai batas menjeda setiap situs di akun sampai Anda upgrade.
Dokumentasi Netlify menyatakan kiriman formulir gratis dan tanpa batas di paket berbasis kredit. Masalahnya ada di situs itu sendiri: saat tim gratis kehabisan kredit bulanan, proyeknya dijeda dan tidak menerima kiriman formulir.
Bisa. Ikiform tidak peduli di mana halaman di-host. Hapus atribut Netlify dari formulir, atur action-nya ke endpoint Ikiform Anda, dan Netlify Forms berhenti melihatnya.
Tidak. Salinan itu hanya ada agar langkah deploy Netlify bisa menemukan formulir. Ikiform menerima post di sebuah URL, jadi hapus salinannya dan kirim dari komponen Anda.
Tidak, tidak ada paket gratis. Pengujian gratis: Anda bisa mengirim 50 kiriman uji per bulan dari localhost sebelum membayar. Untuk go live, Pro mulai $6/bln untuk 500 kiriman, atau Anda bisa bayar sekali untuk 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.