Scaffold a form that posts through SvelteKit remote functions instead of classic form actions. The generator emits a +page.svelte, a form.remote.ts handler, a Zod schema, and a server test from one field list. Standard field types — text, number, email, editor, select, bool, file — render with matching shadcn-svelte inputs, and file fields switch the form to multipart/form-data automatically. Also patches svelte.config.js to enable the experimental remote-functions and async compiler options if they are not already enabled.
<script lang="ts">
import { submitContactForm } from "./form.remote";
import { Button } from "#lib/components/ui/button/index.js";
import { Input } from "#lib/components/ui/input/index.js";
import { Textarea } from "#lib/components/ui/textarea/index.js";
</script>
<section data-role="content">
<div class="flex justify-between items-center mb-4">
<h1 class="text-3xl font-bold tracking-tight">Contact</h1>
</div>
<div class="bg-card rounded-lg shadow-sm border p-4">
<form {...submitContactForm}>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2 col-span-1">
<label for="name" class="text-sm font-medium">Name</label>
<Input id="name" {...submitContactForm.fields.name.as("text")} />
{#each submitContactForm.fields.name.issues() as issue}
<p class="text-destructive text-sm">{issue.message}</p>
{/each}
</div>
<div class="space-y-2 col-span-1">
<label for="email" class="text-sm font-medium">Email</label>
<Input id="email" {...submitContactForm.fields.email.as("email")} />
{#each submitContactForm.fields.email.issues() as issue}
<p class="text-destructive text-sm">{issue.message}</p>
{/each}
</div>
<div class="space-y-2 col-span-2">
<label for="message" class="text-sm font-medium">Message</label>
<Textarea
id="message"
{...submitContactForm.fields.message.as("text")}
/>
{#each submitContactForm.fields.message.issues() as issue}
<p class="text-destructive text-sm">{issue.message}</p>
{/each}
</div>
</div>
<div class="mt-4 flex gap-2 border-t pt-4 -mx-4 px-4">
<Button type="submit">Submit</Button>
</div>
</form>
</div>
</section>
import { form, getRequestEvent } from "$app/server";
import { setFlash } from "sveltekit-flash-message/server";
import { contactSchema } from "#lib/schemas/contact.js";
export const submitContactForm = form(contactSchema, async (data) => {
const { cookies } = getRequestEvent();
setFlash({ type: "toast", message: "Form posted successfully" }, cookies);
return { success: true };
});
import { beforeEach, describe, expect, it } from "vitest";
describe("/contact", () => {
beforeEach(async (context) => {
await context.agent.authenticateUser();
});
describe("GET /contact", () => {
it("should return a 200 status code", async (context) => {
const response = await context.agent.get("/contact");
expect(response.status).toBe(200);
});
});
});
import { z } from "zod";
export const contactSchema = z.object({
name: z.string().optional(),
email: z.email().optional(),
message: z.string().optional(),
});
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import { sveltekit } from "@sveltejs/kit/vite";
import adapter from "@sveltejs/adapter-node";
export default defineConfig({
plugins: [
tailwindcss(),
sveltekit({
compilerOptions: {
runes: ({ filename }) =>
filename.split(/[/\\]/).includes("node_modules") ? undefined : true,
experimental: {
async: true,
},
},
adapter: adapter(),
// The public origin, which SvelteKit checks form posts against and uses
// for prerendered pages. `vela deploy` sets VELA_ORIGIN, and the value is
// baked in at build time. It is left unset for a deploy that serves more
// than one host, where each request's own origin is used instead.
...(process.env.VELA_ORIGIN
? { paths: { origin: process.env.VELA_ORIGIN } }
: {}),
experimental: {
remoteFunctions: true,
},
}),
],
});