Formulir feedback untuk website atau aplikasi7 menit baca
Buat formulir feedback dalam aplikasi di Astro
Formulir feedback dalam aplikasi Astro: action yang mengambil pengguna dari locals middleware, halaman dari request, dan layar dari pola route.
Tulis Astro action dengan accept: "form" yang memvalidasi rating dengan Zod, membaca pengguna yang login dari context.locals, dan meneruskan jawaban ke backend formulir Anda dengan secret dari astro:env. Render formulir dengan action diatur ke action tersebut, agar bisa mengirim tanpa JavaScript, dan tambahkan script pendek yang memanggil action yang sama di tempat saat JavaScript tersedia. Halaman yang dibahas jawaban berasal dari Referer request, dan Astro.routePattern menamai layarnya.
Tab Astro di panduan utama menanyakan kepada pengunjung mana pun seberapa berguna sebuah halaman. Halaman ini menaruh feedback di dalam aplikasi Astro dengan pengguna yang login, tempat server yang seharusnya menentukan siapa penulisnya dan formulir hanya meminta skor dan komentar. Untuk key dan penyiapan formulir, panduan dokumentasi Astro punya detailnya.
Siapa mengisi apa
Astro action adalah endpoint publik, bisa dijangkau di URL yang dinamai sesuai action-nya. Siapa pun bisa memanggil sendFeedback dengan body rekayasa, jadi action tidak bisa memercayai ID pengguna yang diberikan kepadanya. Pisahkan nilai berdasarkan siapa yang bisa dipercaya untuk menyediakannya:
| Nilai | Diatur oleh | Cara |
|---|---|---|
rating, comment | Pengguna | Divalidasi oleh schema Zod milik action |
user_id, email | Middleware | context.locals.user, dari cookie sesi di setiap request |
page | Request | Header Referer, yaitu halaman tempat formulir berada |
screen | Render halaman | Astro.routePattern, seperti /app/projects/[id]/settings, di input tersembunyi |
routePattern adalah alasan Astro terasa nyaman di sini. Ia menamai file yang merender halaman, sehingga layar pengaturan setiap proyek masuk ke satu label sementara page menyimpan alamat aslinya. Nilai ini melewati browser, jadi schema memeriksa bentuknya dan mengganti yang janggal dengan unknown.
- Tanpa JavaScript, browser mengirim formulir ke halaman saat ini, dengan nama action di URL.
- Middleware berjalan lebih dulu di setiap request dan menaruh pengguna yang login di context.locals.
- Action memvalidasi rating dan layar, menambahkan pengguna dan halaman, lalu mengirim ke Ikiform dengan secret.
- Astro merender halaman lagi, dan Astro.getActionResult() menampilkan baris terima kasih atau error-nya.
- Dengan JavaScript, script memanggil action yang sama lewat fetch. Middleware dan handler berjalan persis seperti sebelumnya.
- Script mengganti formulir dengan baris terima kasih, atau menulis error di bawahnya, tanpa reload.
Setup proyek
src/
actions/index.ts
components/FeedbackForm.astro
middleware.ts
env.d.ts
pages/app/projects/[id]/settings.astro
astro.config.mjs
.envDeklarasikan secret di schema astro:env. Astro kemudian menolak berjalan tanpanya, memberi tipe pada import-nya, dan menjauhkannya dari bundle klien. Adapter server apa pun bisa dipakai; yang ditampilkan adalah Node.
import { defineConfig, envField } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
adapter: node({ mode: "standalone" }),
env: {
schema: {
IKIFORM_SECRET_KEY: envField.string({ context: "server", access: "secret" }),
},
},
});Taruh IKIFORM_SECRET_KEY=ik_sk_... di .env untuk pengembangan lokal. Buat key di API keys dengan scope submissions:write. Lalu tempel definisi ini di tab Fields formulir:
Pratinjau
Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.
Bangun
Taruh pengguna di locals
Middleware berjalan sebelum setiap halaman dan action. Baca sesi Anda di sana sekali, dengan apa pun yang disediakan kode auth Anda, dan setiap handler mendapat hasilnya. readSession mewakili kode itu. Sesi bawaan Astro juga bisa dipakai: baca context.session di action sebagai gantinya.
import { defineMiddleware } from "astro:middleware";
import { readSession } from "./lib/auth";
export const onRequest = defineMiddleware(async (context, next) => {
context.locals.user = await readSession(context.cookies.get("session")?.value);
return next();
});declare namespace App {
interface Locals {
user: { id: string; email: string } | null;
}
}Action-nya
import { ActionError, defineAction } from "astro:actions";
import { IKIFORM_SECRET_KEY } from "astro:env/server";
import { z } from "astro/zod";
function pageFrom(referer: string | null) {
try {
const url = new URL(referer ?? "");
return url.origin + url.pathname;
} catch {
return "";
}
}
export const server = {
sendFeedback: defineAction({
accept: "form",
input: z.object({
rating: z.coerce.number().int().min(1, "Pick a score from 1 to 5.").max(5, "Pick a score from 1 to 5."),
comment: z.string().max(4000).nullish(),
screen: z.string().regex(/^\/[a-z0-9/[\]._-]{0,119}$/).catch("unknown"),
_gotcha: z.string().nullish(),
}),
handler: async (input, context) => {
const user = context.locals.user;
if (!user) throw new ActionError({ code: "UNAUTHORIZED", message: "Your session ended. Sign in again, then send it." });
const origin = context.request.headers.get("origin");
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 ? { Origin: origin } : {}),
},
body: JSON.stringify({
rating: input.rating,
comment: input.comment ?? "",
page: pageFrom(context.request.headers.get("referer")),
screen: input.screen,
user_id: user.id,
email: user.email,
_gotcha: input._gotcha ?? "",
}),
signal: AbortSignal.timeout(8000),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) return { sent: true };
throw new ActionError({
code: "BAD_GATEWAY",
message: result?.error?.message ?? "We couldn't send it. Try again in a moment.",
});
},
}),
};- Argumen kedua handler adalah konteks action. Isinya
locals,request, dancookies, tetapi tidak adaredirect, jadi sesi yang tidak ada menjadi errorUNAUTHORIZEDyang bisa ditampilkan formulir. .catch("unknown")padascreenmengubah label palsu menjadiunknownalih-alih menggagalkan seluruh jawaban.- Input formulir yang kosong sampai ke action
formsebagainull, itulah sebabnyacommentdan honeypot bersifatnullish. - Baik form post maupun panggilan dari script dikirim oleh halaman, jadi
Refererberisi alamatnya di kedua kasus.pageFrommenyimpan origin dan path-nya. Originditeruskan agar uji lokal dihitung sebagailocalhost. Penjelasan lebih lanjut di bawah.
Komponen formulir
---
import { actions, isInputError } from "astro:actions";
const result = Astro.getActionResult(actions.sendFeedback);
const scores = ["Broken", "Frustrating", "Fine", "Good", "Great"];
---
{result?.data?.sent ? (
<p role="status">Thanks. We read every one.</p>
) : (
<form method="POST" action={actions.sendFeedback} data-feedback>
<fieldset>
<legend>How is this screen working for you?</legend>
{scores.map((label, index) => (
<label><input type="radio" name="rating" value={index + 1} required /> {label}</label>
))}
</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={Astro.routePattern} />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<p role="alert">
{result?.error && (isInputError(result.error) ? result.error.fields.rating?.[0] : result.error.message)}
</p>
<button>Send feedback</button>
</form>
)}
<script>
import { actions } from "astro:actions";
const form = document.querySelector<HTMLFormElement>("form[data-feedback]");
form?.addEventListener("submit", async (event) => {
event.preventDefault();
const button = form.querySelector("button")!;
const alert = form.querySelector("[role=alert]")!;
button.disabled = true;
button.textContent = "Sending";
const { error } = await actions.sendFeedback(new FormData(form));
if (!error) {
form.replaceWith(Object.assign(document.createElement("p"), { role: "status", textContent: "Thanks. We read every one." }));
return;
}
alert.textContent = error.message;
button.disabled = false;
button.textContent = "Send feedback";
});
</script>action={actions.sendFeedback}membuat formulir bekerja tanpa JavaScript sama sekali. Astro menangani kiriman dan merender halaman dengan hasilnya.isInputErrormemisahkan kegagalan schema, yang punyafields, dari error yang dilempar handler, seperti pesan sesi.- Script meng-import
actionsdariastro:actionsdan memanggil handler yang sama dengan data formulir. Astro mem-bundle-nya, jadi tidak ada file tambahan. - Submit tanpa JavaScript meninggalkan halaman sebagai hasil POST, dan refresh akan meminta pengiriman ulang. Jika itu mengganggu Anda,
getActionContextdi middleware memungkinkan Anda menyimpan hasilnya dan me-redirect.
Halamannya
---
import AppLayout from "../../../../layouts/AppLayout.astro";
import FeedbackForm from "../../../../components/FeedbackForm.astro";
export const prerender = false;
if (!Astro.locals.user) return Astro.redirect("/sign-in");
---
<AppLayout title="Project settings">
<h1>Project settings</h1>
<aside>
<FeedbackForm />
</aside>
</AppLayout>Mode rendering
Form action hanya bekerja di halaman yang dirender on demand. Halaman yang di-prerender dibangun sekali, jadi tidak ada request yang bisa dikirimi. Dengan output statis default, tandai setiap halaman aplikasi dengan export const prerender = false, atau atur output: "server" jika sebagian besar situs berada di balik login. Middleware tetap berjalan di halaman yang di-prerender saat build, ketika tidak ada cookie, jadi locals.user bernilai null di sana.
Honeypot, pemeriksaan manusia, dan batas
Input tersembunyi _gotcha diteruskan, dan yang terisi disimpan sebagai spam. Pemeriksaan manusia tidak pernah berjalan untuk kiriman dengan secret key, dan kewajiban login menggantikan tugasnya. Secara default Astro memeriksa Origin dari form post ke halaman on demand, sehingga situs lain tidak bisa mengirim formulir di browser pengguna.
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.
Deploy
| Host | Adapter | Secret-nya |
|---|---|---|
| Vercel | @astrojs/vercel | Tambahkan IKIFORM_SECRET_KEY di environment variable proyek. Jika Anda memakai sesi Astro, konfigurasikan storage driver; Vercel tidak punya default. |
| Netlify | @astrojs/netlify | Tambahkan di environment variable situs. Sesi secara default memakai Netlify Blobs. |
| Cloudflare | @astrojs/cloudflare | Jalankan npx wrangler secret put IKIFORM_SECRET_KEY dan taruh di .dev.vars secara lokal. astro:env/server membacanya, begitu juga env dari cloudflare:workers. Sesi memakai binding KV bernama SESSION. |
Sejak Astro 6, Astro.locals.runtime.env sudah dihapus dari adapter Cloudflare. Kode yang membaca key dari sana perlu import astro:env di atas.
Uji lokal gratis
Workspace tanpa paket mendapat 50 kiriman uji gratis per bulan dari localhost, dan Ikiform mengenalinya dari header Origin. Browser mengirim http://localhost:4321 ke astro dev, tetapi fetch milik action ke Ikiform tidak mengirim origin. Baris Origin meneruskan origin browser, jadi uji lokal gratis, dan di produksi header-nya adalah domain Anda sendiri.
Pastikan berfungsi
- Jalankan
astro dev, login, buka halaman pengaturan sebuah proyek, dan kirim nilai 2 dengan komentar. - Di Submissions, pastikan
screenbernilai/app/projects/[id]/settings,pageberisi ID asli, sertauser_iddanemailadalah milik Anda. - Matikan JavaScript dan kirim lagi. Halaman dimuat ulang dengan baris terima kasih.
- Hapus cookie sesi lalu kirim. Formulir menampilkan pesan login.
- Balas email notifikasi. Balasannya ditujukan ke pengguna yang login.
Hal yang sering menjebak
| Masalah | Yang harus dilakukan |
|---|---|
| Formulir terkirim tetapi halaman tidak menampilkan hasil | Halaman di-prerender. Tambahkan export const prerender = false atau beralih ke output: "server". |
IKIFORM_SECRET_KEY is missing saat build dimulai | Secret divalidasi setiap kali astro:env/server di-import. Sediakan variabel itu saat build, atau placeholder di sana dan nilai aslinya saat runtime. |
402 plan_required pada uji lokal | Origin yang diteruskan kosong atau bukan localhost. Buka aplikasi di localhost:4321 dan pertahankan baris Origin. |
locals.user selalu null di produksi | Nama atau domain cookie berbeda antar-environment, atau halaman masih di-prerender. Log context.cookies.has("session") di middleware. |
422 validation_failed | Kiriman tidak cocok dengan definisi field formulir. error.fields menyebutkan setiap field dan alasannya. Samakan name input dengan ID field. |
Pertanyaan
Atur di context.locals di src/middleware.ts, lalu baca context.locals.user dari argumen kedua handler. Sesi Astro adalah jalur lainnya, lewat context.session.
Ya, di halaman yang dirender on demand. Atur action formulir ke action tersebut dan method-nya ke POST, lalu baca hasilnya dengan Astro.getActionResult() di halaman.
Render Astro.routePattern ke input tersembunyi. Nilainya adalah pola file route, seperti /app/projects/[id]/settings, sehingga jawaban dikelompokkan per layar, bukan per ID.
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.