Fitur & Build Notes · · 3 menit baca
Membangun form kontak HTMX di Cloudflare Workers — tanpa SPA
Dokumentasi lengkap form kontak tanpa SPA: HTMX partial swap, validasi server dengan Hono JSX, input tidak hilang saat error, dan progressive enhancement — dengan source code dari situs ini.
Form kontak di situs ini tidak memakai SPA sama sekali: HTMX mengirim POST, server merender ulang partial HTML, dan halaman tidak pernah reload. Validasi terjadi di server (satu-satunya tempat yang bisa dipercaya), dan input pengunjung tidak hilang saat ada error. Ini dokumentasi lengkap cara membangunnya di Cloudflare Workers dengan Hono JSX.
Arsitekturnya
Browser ──POST (hx-post)──▶ Worker (Hono)
▲ │ validasi server
└──── partial HTML ◀─────────┘ re-render form (error) / kartu sukses
Satu endpoint, tiga kemungkinan respons: form dengan pesan error (422), kartu sukses (200), atau error pengiriman (500). HTMX menukar isi #contact-form-wrap dengan respons apa pun yang datang.
1. Form dengan atribut HTMX
State form dipegang satu interface — nilai input plus map error — sehingga form bisa dirender ulang dengan isian pengunjung tetap utuh:
export interface ContactForm {
name: string;
email: string;
message: string;
// ...field lain
errors: Record<string, string>;
}
export const ContactFormView = ({ f, locale }: { f: ContactForm; locale: Locale }) => (
<form hx-post={href(locale, "/contact")} hx-target="#contact-form-wrap"
hx-swap="outerHTML" class="space-y-6" novalidate>
{/* setiap Field menampilkan f.errors[field] di bawah inputnya */}
</form>
);
novalidate sengaja: validasi browser dimatikan supaya semua pesan error datang dari server — konsisten di semua browser dan tetap jalan tanpa JavaScript.
2. Handler POST di Worker
contactRoutes.post(path, async (c) => {
const form = await c.req.parseBody();
const f: ContactForm = {
name: val("name").trim(),
email: val("email").trim(),
message: val("message").trim(),
errors: {},
};
f.errors = validateContact(f, locale);
if (Object.keys(f.errors).length > 0) {
return c.html(<ContactFormView f={f} locale={locale} />, 422); // input utuh + error
}
await sendContact(c.env, f); // Resend HTTP API (log-only saat dev)
return c.html(<ContactSuccess f={f} locale={locale} />);
});
Perhatikan: yang dikembalikan bukan JSON, tapi HTML jadi. Tidak ada state management di browser, tidak ada useState, tidak ada duplikasi logika validasi.
3. Validasi server
function validateContact(f: ContactForm, locale: Locale): Record<string, string> {
const msgs = resolveContact(locale).errors; // pesan error ikut bahasa halaman
const errs: Record<string, string> = {};
if (f.name === "") errs.name = msgs.name;
if (!EMAIL_RE.test(f.email)) errs.email = msgs.emailInvalid;
if (f.message.length < 10) errs.message = msgs.message;
return errs;
}
Kenapa pola ini kupakai
- Ringan: HTMX 14KB vs framework SPA ratusan KB — di 4G bedanya terasa.
- Satu sumber kebenaran: validasi cuma ditulis sekali, di server.
- Progressive enhancement: tanpa JavaScript, form tetap ter-submit sebagai POST biasa.
- Cocok untuk Workers: respons HTML kecil, cold start nol, tanpa build step client.
Mau fitur seperti ini dibangun untuk bisnismu?