Daftar tunggu untuk SaaS baru6 menit baca
Membuat daftar tunggu di SvelteKit
Daftar tunggu SvelteKit 3: hook menyimpan ?ref= pertama di cookie, form action bernama meneruskan pendaftaran, dan pengunjung yang kembali melihat bahwa mereka sudah terdaftar.
Baca ?ref= di hooks.server.ts pada request pertama pengunjung dan simpan di cookie, sehingga pendaftaran dikreditkan ke tautan yang membawa mereka meskipun mereka sudah menjelajah ke halaman lain. Form action bernama membaca cookie itu, meneruskan pendaftaran ke backend formulir Anda dengan secret key, dan memasang cookie kedua sehingga halaman menyapa pengunjung yang kembali alih-alih bertanya lagi.
Panduan utama punya tab SvelteKit: default action dan use:enhance, dengan ref tetap bernilai homepage. Halaman ini menambahkan apa yang dibutuhkan peluncuran sungguhan. Pengunjung tiba di posting blog dari newsletter, membaca sebentar, lalu mendaftar dua halaman kemudian. Anda ingin pendaftaran itu dikreditkan ke newsletter, bukan ke halaman tempat mereka mengetik email.
Apa yang berbeda di SvelteKit
Dua fitur SvelteKit sangat cocok untuk ini, dan satu aturan menentukan penyiapannya:
| Fitur | Fungsinya di sini |
|---|---|
handle di hooks.server.ts | Berjalan sebelum setiap request yang dirender di server, jadi bisa melihat ?ref= di halaman mana pun tempat pengunjung mendarat |
event.cookies | Menyimpan sumber pertama selama 30 hari. Action membacanya kembali, tanpa input tersembunyi yang bisa diutak-atik |
Form action dengan use:enhance | Mengirim tanpa JavaScript dan ditingkatkan saat JavaScript dimuat, dengan satu potong kode server |
| Aturannya | Halaman dengan action tidak bisa di-prerender, karena server harus menjawab POST-nya |
Input tersembunyi tidak bisa melakukan ini. Ia hanya tahu halaman tempat formulir berada, dan tag di URL hilang setelah klik pertama. Cookie yang dipasang di server menyimpannya di setiap halaman, dan pengunjung tidak pernah melihat atau mengeditnya.
Ini atribusi first-touch: tautan bertag pertama yang menang. Kunjungan berikutnya dari tautan lain tidak menimpanya. Jika Anda lebih suka mengkreditkan tautan terakhir, hapus pengecekan !event.cookies.get(...).
- Pengunjung mendarat di posting blog dari tautan dengan ?ref=news.
- Hook handle melihat tag itu, belum menemukan cookie waitlist_ref, dan memasangnya selama 30 hari.
- Mereka membaca halaman harga lalu kembali ke beranda. Kedua URL itu tidak punya tag; cookie masih menyimpannya.
- Mereka mendaftar. Action join membaca waitlist_ref dan meneruskan pendaftaran ke Ikiform dengan ref bernilai news.
- Jika sukses, action memasang waitlist_joined, sehingga beranda menampilkan kalimat terima kasih pada kunjungan berikutnya alih-alih formulir.
Siapkan proyek
Buat formulir dan secret key seperti di panduan SvelteKit di dokumentasi. Halaman ini memakai SvelteKit 3. File-filenya:
src/
env.ts
hooks.server.ts
routes/
+page.server.ts
+page.svelte
blog/[slug]/+page.svelte
vite.config.ts
.envDeklarasikan secret
SvelteKit 3 menandai modul $env/... sebagai deprecated dan menggantinya dengan variabel yang dideklarasikan. Deklarasikan key di src/env.ts; variabel bersifat privat kecuali Anda menandainya publik, dan $app/env/private tidak bisa diimpor ke kode browser.
import { defineEnvVars } from "@sveltejs/kit/env";
export const variables = defineEnvVars({
IKIFORM_SECRET_KEY: {},
});IKIFORM_SECRET_KEY=ik_sk_your_secret_keyIKIFORM_SECRET_KEY di environment variable khusus server.Action mengirim email dan ref. Jika formulir punya definisi field di tab Fields, cantumkan keduanya:
{
"version": 1,
"title": "Waitlist",
"fields": [
{ "id": "email", "type": "email", "label": "Email", "required": true, "placeholder": "you@company.com" },
{ "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
]
}Bangun
Tangkap sumbernya
import type { Handle } from "@sveltejs/kit/hooks";
const SOURCE = /^[a-z0-9_-]{1,40}$/;
const THIRTY_DAYS = 60 * 60 * 24 * 30;
export const handle: Handle = async ({ event, resolve }) => {
const query = event.url.searchParams;
const tag = (query.get("ref") ?? query.get("utm_source") ?? "").toLowerCase();
if (SOURCE.test(tag) && !event.cookies.get("waitlist_ref")) {
event.cookies.set("waitlist_ref", tag, { maxAge: THIRTY_DAYS, httpOnly: true, sameSite: "lax" });
}
return resolve(event);
};- Tag diubah menjadi huruf kecil dan harus cocok dengan pola yang sama yang diizinkan
max_lengthIkiform, jadi tautan yang direkayasa tidak bisa menyimpan sampah. httpOnlymenjauhkan cookie dari skrip halaman.sameSite: "lax"tetap mengirimnya padaPOSTmilik formulir itu sendiri.- Di SvelteKit 3,
pathcookie default-nya/, jadi cookie menjangkau setiap route tanpa perlu diatur.
Action bernama
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions, PageServerLoad } from "./$types";
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const YEAR = 60 * 60 * 24 * 365;
export const load: PageServerLoad = ({ cookies }) => ({
joined: cookies.get("waitlist_joined") === "yes",
});
export const actions = {
join: async ({ request, cookies, fetch, url }) => {
const form = await request.formData();
const email = String(form.get("email") ?? "").trim();
if (!EMAIL.test(email)) return fail(400, { email, message: "Enter the email we should send your invite to." });
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
Origin: request.headers.get("origin") ?? url.origin,
},
body: JSON.stringify({
email,
ref: cookies.get("waitlist_ref") ?? "direct",
_gotcha: String(form.get("_gotcha") ?? ""),
}),
signal: AbortSignal.timeout(8000),
}).catch(() => null);
if (!response) return fail(504, { email, message: "We couldn't reach the list. Try again." });
const result = await response.json().catch(() => null);
if (!result?.ok) {
return fail(response.status === 429 ? 429 : 400, {
email,
message: result?.error?.fields?.email?.[0] ?? result?.error?.message ?? "Couldn't join the list. Try again.",
});
}
cookies.set("waitlist_joined", "yes", { maxAge: YEAR, httpOnly: true, sameSite: "lax" });
return { joined: true };
},
} satisfies Actions;- Action bernama,
join, menyisakan ruang untuk action lain di halaman ini nanti. SvelteKit tidak mengizinkan default action berdampingan dengan action bernama. - Header
Originbrowser diteruskan karena Ikiform menghitung kiriman uji gratis darilocalhostberdasarkan header itu, danfetchmilik action tidak mengirimnya. fail()mengembalikan email yang diketik dan sebuah pesan. Di SvelteKit 3, submit yang di-enhance memakai status yang Anda berikan kefail(), jadipage.statusbernilai400atau429.loadmembacawaitlist_joinedagar pengunjung yang kembali melihat bahwa mereka sudah terdaftar. Ini hanya kesopanan untuk satu browser, bukan deduplikasi; Ikiform tetap menyimpan pendaftaran kedua dari perangkat lain.
Halamannya
<script lang="ts">
import { enhance } from "$app/forms";
import type { PageProps } from "./$types";
let { data, form }: PageProps = $props();
let pending = $state(false);
</script>
<h1>Acme is almost here</h1>
{#if data.joined || form?.joined}
<p role="status">You're on the list. We'll email you on launch day.</p>
{:else}
<form
method="POST"
action="?/join"
use:enhance={() => {
pending = true;
return async ({ update }) => {
await update({ reset: false });
pending = false;
};
}}
>
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required value={form?.email ?? ""} />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
{#if form?.message}
<p role="alert">{form.message}</p>
{/if}
<button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
</form>
{/if}action="?/join"menamai action-nya. Tanpa JavaScript, browser mengirim ke sana dan SvelteKit merender halaman denganformterisi.use:enhancemengirim denganfetchsetelah halaman di-hydrate.update({ reset: false })mempertahankan email di input setelah error, jadi pengunjung cukup memperbaiki salah ketik tanpa mengetik ulang.pendingmenonaktifkan tombol sampai action menjawab, yang mencegah kiriman ganda di ponsel yang lambat.data.joinedberasal dariloaddanform.joineddari action. Memeriksa keduanya mencakup pengunjung yang kembali dan pendaftaran baru dengan satu kondisi.
Honeypot sampai ke Ikiform tanpa diubah, jadi bot yang mengisinya disimpan sebagai spam. Kiriman dengan secret key melewati pemeriksaan manusia Ikiform, jadi mengaktifkan Spam check tidak menambah apa pun di sini.
Prerendering dan deploy
Beranda punya action, jadi harus dirender di server. Posting blog dan konten lain bisa di-prerender dengan export const prerender = true, tetapi handle tidak berjalan saat pengunjung membuka file yang di-prerender; ia hanya berjalan sekali saat build. Tautan bertag yang langsung menuju halaman yang di-prerender tidak memasang cookie. Arahkan tautan kampanye ke halaman yang dirender di server, seperti beranda.
| Host | Adapter | Catatan |
|---|---|---|
| Vercel | @sveltejs/adapter-vercel | SvelteKit 3 menghapus runtime edge di sini, jadi action berjalan di Node functions |
| Netlify | @sveltejs/adapter-netlify | Atur key di Environment variables |
| Cloudflare | @sveltejs/adapter-cloudflare | Memerlukan wrangler 4.118 atau lebih baru, peer dependency adapter. Atur key dengan wrangler secret put IKIFORM_SECRET_KEY |
Pastikan berfungsi
- Jalankan
vite dev, bukahttp://localhost:5173/?ref=test, lalu hapus query dan reload. Cookiewaitlist_refmasih bernilaitestdi dev tools browser. - Mendaftar. Kalimat terima kasih menggantikan formulir tanpa reload, dan pendaftaran di Submissions memiliki
refbernilaitest. - Reload halaman. Yang muncul kalimat terima kasih, bukan formulir.
- Hapus cookie, matikan JavaScript, dan kirim
a@b. Halaman kembali dengan error dan alamat masih ada di input. - Kirim dengan honeypot terisi,
_gotcha=bot. Kiriman tetap mendapat jawaban sukses, tetapi disimpan sebagai spam dan tidak memicu email atau webhook. - Kiriman dari
vite devmembawa originlocalhost, jadi dihitung sebagai kiriman uji. Ikiform tidak punya paket gratis. Pengujian gratis: workspace tanpa paket menerima 50 kiriman uji per bulan darilocalhost. 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.
Jebakan
| Masalah | Yang harus dilakukan |
|---|---|
| Build gagal dengan pesan bahwa halaman dengan action tidak bisa di-prerender | Sebuah layout mengatur prerender = true untuk setiap route. Atur export const prerender = false di +page.server.ts halaman daftar tunggu. |
ref selalu direct | Tautan bertag mengarah ke halaman yang di-prerender, jadi handle tidak pernah melihatnya, atau kunjungan sebelumnya sudah memasang cookie. Periksa cookie di dev tools. |
Peringatan $env/static/private setelah upgrade | SvelteKit 3 menandai modul $env sebagai deprecated. Deklarasikan key di src/env.ts dan impor dari $app/env/private. |
422 dengan unknown_field | Definisi field tidak mencantumkan ref. Tambahkan seperti pada definisi di atas. |
402 plan_required | Workspace tidak punya paket dan kiriman berasal dari situs live. Kiriman ditahan, tidak hilang. Pilih paket atau beli paket kiriman di Settings โบ Plan and usage, dan kiriman yang ditahan akan masuk. |
Pertanyaan
Baca query di hook handle pada request pertama, simpan tag di cookie httpOnly, dan baca cookie itu di form action. Sumbernya bertahan saat navigasi dan tidak pernah muncul di formulir.
Tidak. Halaman dengan action memerlukan server untuk menjawab POST-nya, jadi SvelteKit menolak melakukan prerender. Prerender halaman lain dan biarkan halaman formulir dirender di server.
Hanya di browser itu. Ikiform tidak melakukan deduplikasi berdasarkan email, jadi hapus duplikat di tempat daftar itu disimpan atau saat Anda mengekspornya.
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.