Formulir feedback untuk website atau aplikasi7 menit baca
Buat formulir feedback dalam aplikasi di SvelteKit
Formulir feedback dalam aplikasi SvelteKit: formulir di layout mengirim ke satu form action, yang menambahkan pengguna dari locals dan halaman dari request di server.
Taruh formulir di layout yang membungkus layar untuk pengguna login dan kirim ke satu action bernama, /feedback?/send, dengan use:enhance. Hook handle menaruh pengguna di event.locals, sehingga action sendiri yang menambahkan ID dan email mereka serta membaca halaman dari request. Browser hanya mengirim skor, komentar, dan route id.
Tab SvelteKit di panduan utama mengirim JSON ke endpoint +server.ts dari halaman mana pun, login atau tidak. Halaman ini membangun versi untuk bagian dalam aplikasi: form action sungguhan yang bekerja sebelum JavaScript termuat, dengan identitas pengguna diambil dari sesi di server. Ditulis untuk SvelteKit 3 dan Svelte 5. Key dan allowed domains dibahas di panduan dokumentasi SvelteKit.
Mengapa satu action untuk setiap layar
Form action milik sebuah halaman, tetapi formulir feedback ada di layout dan tampil di puluhan halaman. Menyalin action ke setiap +page.server.ts akan berantakan. SvelteKit memungkinkan formulir memanggil action route lain dengan query string, jadi setiap layar mengirim ke /feedback?/send dan satu file memegang logikanya. Setiap nilai kemudian berasal dari tempat yang tepat:
| Nilai | Sumber | Kepercayaan |
|---|---|---|
rating, comment | Formulirnya | Apa yang ingin disampaikan pengguna |
user_id, email | event.locals.user, diatur oleh hook handle | Dibaca dari cookie sesi di server |
page | Referer dari request action | Hanya same-origin; selebihnya dibuang |
screen | page.route.id di input tersembunyi | Label pengelompokan. Diperiksa terhadap pola sebelum disimpan |
page.route.id adalah path file route, seperti /(app)/projects/[id]/settings. Semua halaman pengaturan proyek berbagi nilai ini, itulah yang Anda inginkan saat menghitung rata-rata skor per layar. Action menghapus segmen grup seperti (app), karena itu hanya detail susunan folder Anda.
- Pengguna menilai layar di formulir layout. use:enhance mengirimnya ke /feedback?/send tanpa memuat halaman.
- Hook handle berjalan lebih dulu, membaca cookie sesi, dan menaruh pengguna di event.locals.
- Action send memeriksa bahwa ada pengguna dan skor bulat dari 1 sampai 5. Jika tidak, action mengembalikan fail dengan pesan.
- Action menambahkan ID dan email pengguna, halaman dari Referer, dan route id yang sudah dibersihkan, lalu mengirim dengan secret key.
- update dengan navigate diatur ke false menerapkan hasilnya ke layar saat ini, jadi baris terima kasih tampil di tempat pengguna berada.
Setup proyek
src/
app.d.ts
env.ts
hooks.server.ts
lib/
FeedbackForm.svelte
server/session.ts
routes/
(app)/+layout.svelte
feedback/
+page.server.ts
+page.svelte
.envBuat secret key dengan scope submissions:write di API keys. SvelteKit 3 mendeklarasikan environment variable di src/env.ts. Variabel bersifat privat dan dibaca saat runtime kecuali Anda menyatakan lain, jadi key tidak pernah sampai ke bundle browser dan bisa diubah di host tanpa rebuild. .env mengisinya selama pengembangan.
IKIFORM_SECRET_KEY=ik_sk_your_secret_keyimport { defineEnvVars } from "@sveltejs/kit/env";
export const variables = defineEnvVars({
IKIFORM_SECRET_KEY: {},
});Tempel definisi ini di tab Fields formulir. Formulir menampilkan skor dan komentar; action menyediakan sisanya, dan field email membuat Reply di setiap notifikasi menuju ke pengguna.
Pratinjau
Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.
6 atau field yang tidak ada di definisi, dan preview menampilkan 422 yang diubah action menjadi pesan.Bangun
Taruh pengguna di locals
handle berjalan untuk setiap request yang diterima server, termasuk kiriman form action. Beri tipe pengguna sekali di app.d.ts, lalu baca dari sesi Anda di hook. readSession mewakili apa pun yang sudah dilakukan kode auth Anda dengan cookie.
declare global {
namespace App {
interface Locals {
user: { id: string; email: string } | null;
}
}
}
export {};import type { Handle } from "@sveltejs/kit/hooks";
import { readSession } from "#lib/server/session.js";
export const handle: Handle = async ({ event, resolve }) => {
event.locals.user = await readSession(event.cookies.get("session"));
return resolve(event);
};Form action
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";
export const prerender = false;
const SCREEN = /^[a-z0-9/[\]._-]{1,120}$/;
function pageFrom(referer: string | null, origin: string) {
try {
const url = new URL(referer ?? "");
return url.origin === origin ? url.origin + url.pathname : "";
} catch {
return "";
}
}
export const actions = {
send: async ({ request, locals, url }) => {
const data = await request.formData();
if (!locals.user) return fail(401, { message: "Your session ended. Sign in again, then send it." });
const rating = Number(data.get("rating"));
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
return fail(400, { message: "Pick a score from 1 to 5." });
}
const screen = String(data.get("screen") ?? "").replace(/\/\([^)]+\)/g, "") || "/";
const back = pageFrom(request.headers.get("referer"), url.origin);
let response: Response;
try {
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({
rating,
comment: String(data.get("comment") ?? "").trim().slice(0, 4000),
page: back,
screen: SCREEN.test(screen) ? screen : "unknown",
user_id: locals.user.id,
email: locals.user.email,
_gotcha: String(data.get("_gotcha") ?? ""),
}),
signal: AbortSignal.timeout(8000),
});
} catch {
return fail(502, { message: "We couldn't send it. Try again in a moment." });
}
if (response.status === 429) return fail(429, { message: "Too many answers at once. Try again in a minute." });
const result = await response.json().catch(() => null);
if (!result?.ok) {
return fail(400, { message: result?.error?.fields?.comment?.[0] ?? result?.error?.message ?? "We couldn't send it." });
}
return { sent: true, back };
},
} satisfies Actions;failmengembalikan status dan data tanpa melempar exception. Di SvelteKit 3, formulir yang di-enhance juga menerima status itu, jadi401dan400muncul di tab network sebagaimana adanya.pageFromhanya menyimpanRefereryang berasal dari origin Anda sendiri, dan membuang query string-nya.export const prerender = falsesengaja ada di sana. Halaman dengan action tidak bisa di-prerender, jadi jika root layout Anda melakukan prerender secara default, build gagal tanpanya.- Baris
Originmeneruskan origin browser, dengan fallback ke origin aplikasi sendiri, dan memberi tahu Ikiform dari mana request berasal. Itulah yang membuat uji lokal dihitung sebagailocalhost, di bawah. 429dari Ikiform punya pesan sendiri, karena di balik server artinya formulir secara keseluruhan sedang sibuk, bukan pengguna ini.
Formulir layout
<script lang="ts">
import { enhance } from "$app/forms";
import { page } from "$app/state";
const scores = ["Broken", "Frustrating", "Fine", "Good", "Great"];
let sending = $state(false);
</script>
{#if page.form?.sent}
<p role="status">Thanks. We read every one.</p>
{:else}
<form
method="POST"
action="/feedback?/send"
aria-busy={sending}
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update({ navigate: false, refreshAll: false });
sending = false;
};
}}
>
<fieldset>
<legend>How is this screen working for you?</legend>
{#each scores as label, index}
<label><input type="radio" name="rating" value={index + 1} required /> {label}</label>
{/each}
</fieldset>
<label for="feedback-comment">What would make it better?</label>
<textarea id="feedback-comment" name="comment" rows="4" maxlength="4000"></textarea>
<input type="hidden" name="screen" value={page.route.id ?? ""} />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
{#if page.form?.message}
<p role="alert">{page.form.message}</p>
{/if}
<button disabled={sending}>{sending ? "Sending" : "Send feedback"}</button>
</form>
{/if}- Secara default, formulir yang di-enhance dengan action di halaman lain akan bernavigasi ke sana setelah kiriman.
navigate: falsemenjaga pengguna tetap di layarnya dan menaruh hasilnya dipage.formdi sini. refreshAll: falsemelewati menjalankan ulang setiap fungsiloadsetelah berhasil. Feedback tidak mengubah apa pun yang ditampilkan halaman.- Saat gagal, formulir tidak direset, jadi komentar tetap ada di kotak di samping pesannya.
page.formmilik halaman saat ini. Saat pengguna bernavigasi, nilainya dikosongkan, dan layar berikutnya menampilkan formulir baru.
<script lang="ts">
import FeedbackForm from "#lib/FeedbackForm.svelte";
let { children } = $props();
</script>
<main>{@render children()}</main>
<aside>
<FeedbackForm />
</aside>Tanpa JavaScript, formulir yang sama tetap bekerja. Browser mengirim ke /feedback?/send dan mendarat di /feedback, yang merender hasil dan tautan kembali ke halaman asalnya:
<script lang="ts">
import type { PageProps } from "./$types";
let { form }: PageProps = $props();
</script>
{#if form?.sent}
<p role="status">Thanks. We read every one.</p>
<a href={form.back || "/"}>Back to where you were</a>
{:else if form?.message}
<p role="alert">{form.message}</p>
<a href="/">Back to the app</a>
{:else}
<p>Send feedback from the panel on any screen of the app.</p>
{/if}Honeypot, pemeriksaan manusia, dan batas
Input tersembunyi _gotcha diteruskan tanpa diubah, dan Ikiform menyimpan yang terisi sebagai spam. Pemeriksaan manusia Ikiform dilewati untuk kiriman dengan secret key. Di sini action sudah menolak siapa pun tanpa sesi, yang menahan bot yang akan dihentikan pemeriksaan manusia. Yang tersisa adalah akun yang login mengirim terus-menerus, jadi atur preset rate limit untuk formulir yang mengirim dari satu server.
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.
Rendering dan deploy
Layar di sekitar formulir bisa dirender di server atau di-prerender sesuka Anda; formulir sendiri tidak butuh apa pun saat render. Hanya /feedback yang harus berjalan di server. Adapter diatur di vite.config.ts, di dalam plugin sveltekit().
| Host | Adapter dan key |
|---|---|
| Vercel | @sveltejs/adapter-vercel. Tambahkan IKIFORM_SECRET_KEY di Environment Variables. Edge runtime sudah tidak ada di SvelteKit 3, jadi action berjalan di Node function. |
| Netlify | @sveltejs/adapter-netlify. Atur key di environment variable situs. |
| Cloudflare | @sveltejs/adapter-cloudflare dengan Wrangler 4.118 atau lebih baru, peer dependency-nya. Simpan key sebagai secret Worker, misalnya dengan wrangler secret put IKIFORM_SECRET_KEY, dan baca lewat $app/env/private seperti di atas. |
Uji gratis di localhost
Tanpa paket, workspace menerima 50 kiriman uji per bulan dari localhost. Ikiform menentukannya dari header Origin, dan fetch dari server Anda tidak mengirimnya kecuali Anda mengaturnya. Itulah sebabnya action meneruskan Origin dari request: di vite dev nilainya http://localhost:5173, dan kiriman dihitung sebagai uji gratis. Hilangkan baris itu dan uji lokal kembali dengan 402 plan_required, ditahan sampai Anda memilih paket.
Pastikan berfungsi
- Jalankan
vite dev, login, buka layar pengaturan sebuah proyek, dan kirim skor 2 dengan komentar. - Buka Submissions.
screenberisi/projects/[id]/settings,pageberisi ID proyek asli, sertauser_iddanemailcocok dengan akun Anda. - Hapus cookie sesi dan kirim lagi. Formulir menampilkan pesan login dan tetap menyimpan komentar Anda.
- Matikan JavaScript lalu kirim. Anda mendarat di
/feedbackdengan baris terima kasih dan tautan kembali ke layar. - Balas email notifikasi. Balasannya menuju pengguna yang login.
Hal yang sering menjebak
| Masalah | Yang harus dilakukan |
|---|---|
Ucapan terima kasih tampil di /feedback alih-alih di layar saat ini | Callback enhance memanggil update() tanpa navigate: false, atau formulirnya tidak di-enhance. Periksa blok use:enhance. |
locals.user selalu null di action | Hook membaca nama cookie yang berbeda, atau path cookie tidak mencakup /feedback. SvelteKit 3 mengatur cookie baru ke / secara default, tetapi cookie yang diatur kode lama mungkin dibatasi ke subpath. |
| Kiriman dari domain lain milik Anda ditolak | SvelteKit selalu memeriksa origin form post. Tambahkan origin itu ke csrf.trustedOrigins alih-alih membukanya untuk semua orang. |
Build gagal dengan error tentang prerendering /feedback | Halaman dengan action tidak bisa di-prerender. Pertahankan export const prerender = false di +page.server.ts-nya. |
422 validation_failed | Kiriman tidak cocok dengan definisi field formulir. error.fields menyebutkan setiap field dan alasannya. Samakan name input dengan ID field. |
Pertanyaan
Bisa. Atur action formulir ke route lain ditambah nama action-nya, seperti /feedback?/send. Dengan use:enhance, teruskan navigate: false ke update agar hasilnya tampil di halaman saat ini alih-alih bernavigasi ke sana.
Baca sesi di hook handle dan atur event.locals.user. Setiap action menerima locals, jadi action bisa melampirkan ID dan email pengguna tanpa memercayai apa pun yang dikirim browser.
Halaman dengan form action tidak bisa di-prerender, karena server harus menjawab POST-nya. Atur prerender ke false di route yang memegang action.
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.