Catatan teknis · DikaGlorya (DikaOfc)
Formulir kontak tanpa backend: brief HTML lewat clipboard dan fallback mailto
Cara kerja formulir kontak ObitoGlory di situs statis: validasi lokal, brief HTML yang disalin ke clipboard untuk Gmail, fallback mailto, dan batasan yang perlu diketahui.
Masalahnya: situs statis tidak memiliki kotak masuk
Situs ini di-host sebagai file statis, jadi tidak ada server yang dikendalikan origin untuk menerima POST formulir. Formulir kontak memiliki tiga pilihan realistis: layanan formulir pihak ketiga, endpoint yang di-host sendiri, atau tidak menyediakan formulir.
Layanan pihak ketiga membutuhkan skrip eksternal, akun, dan kebijakan privasi yang perlu ditinjau. Backend membuat portofolio statis memiliki log, penyaringan spam, dan kebutuhan pemeliharaan. Pilihan di sini adalah jalan tengah: formulir menyusun brief terstruktur, menyalin versi HTML ke clipboard, lalu menyerahkannya lewat Gmail atau mailto ke aplikasi email yang sudah digunakan pengunjung. Alamat email tetap ditampilkan di halaman, jadi tidak ada informasi baru yang dibuka.
Pertahankan formulir yang tetap berfungsi tanpa JavaScript
Elemen formulir mendeklarasikan fallback sebelum skrip apa pun berjalan: aksi mailto dengan encoding text/plain. Jika skrip tidak pernah dimuat, browser tetap mencoba pengiriman mailto bawaan. Hasilnya berbeda-beda antarbrowser dan tidak selalu mulus, tetapi tetap menyediakan jalur dasar.
<form
action="mailto:dikasukasukaa@gmail.com"
method="post"
enctype="text/plain"
novalidate
data-contact-form
data-contact-email={site.email}
>
Tangani submit dan validasi secara lokal
Atribut novalidate menonaktifkan pesan bawaan agar handler kustom selalu berjalan. checkValidity dan reportValidity mempertahankan perilaku bawaan yang penting: fokus berpindah ke kolom tidak valid pertama dan browser menjelaskan kesalahannya. Baris status menyampaikan pesan dengan gaya bahasa yang konsisten di seluruh halaman.
Satu detail migrasi: pilihan layanan dinormalisasi lebih dulu dari query string. URL Indonesia lama seperti ?service=perbaikan dipetakan ke repairs, sehingga tautan lama tetap memilih opsi yang tepat dan tidak gagal tanpa penjelasan.
form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
event.preventDefault();
form.reportValidity();
setStatus('Lengkapi kolom wajib terlebih dahulu.', true);
return;
}
event.preventDefault();
// ...susun draft mailto dari FormData
});
Susun brief HTML lalu salin ke clipboard
Brief disusun dari FormData dan dirapikan dengan cara yang sama, apa pun kolom sumbernya. Tautan mailto dan URL compose Gmail hanya membawa teks biasa, jadi keduanya tidak bisa membuka draft berformat HTML. Jalur yang dipakai adalah Clipboard API: brief dibuat sebagai tabel HTML dengan inline style, lalu ditulis ke clipboard dalam dua representasi sekaligus, text/html dan text/plain.
Representasi HTML mengikuti batasan Gmail: tabel untuk layout, inline style, tanpa flexbox, grid, atau style block. Representasi teks biasa menjadi fallback untuk aplikasi email yang tidak menerima HTML. Subjek tetap melewati encodeURIComponent karena ampersand dan spasi di dalamnya harus aman di URL.
const item = new ClipboardItem({
'text/html': new Blob([buildBriefFragment(data)], { type: 'text/html' }),
'text/plain': new Blob([buildBriefText(data)], { type: 'text/plain' }),
});
await navigator.clipboard.write([item]);
Fallback yang wajib tersedia
Clipboard API tidak selalu tersedia. Browser sandbox, webview tertanam, konteks non-HTTPS, atau izin yang ditolak bisa membuat penulisan gagal. Karena itu handler mencoba rich HTML lebih dulu, lalu teks biasa, lalu menampilkan instruksi salin manual. Panel hasil tidak pernah mengklaim brief sudah terkirim.
Panel menyediakan tautan Gmail, tombol salin ulang, dan fallback mailto berisi teks biasa. Tautan Gmail hanya membawa penerima dan subjek, bukan badan email, karena URL compose tidak menerima HTML. Pengunjung menempel brief sendiri di badan email, dan tetap bisa memeriksa isinya sebelum menekan Kirim.
try {
await navigator.clipboard.write([item]);
setReadyStatus('Brief HTML disalin. Buka Gmail, lalu tempel.');
} catch {
await navigator.clipboard.writeText(buildBriefText(data));
setReadyStatus('Brief disalin sebagai teks biasa.');
}
Hal yang sudah diverifikasi dan yang belum
Bagian yang dapat diverifikasi ada di repositori ini: pemeriksaan tipe dan build berhasil, pemeriksa tautan internal berhasil, dan catatan pemeriksaan manual di Chromium menunjukkan submit kosong ditolak dengan status kolom wajib, fokus berpindah ke kolom tidak valid pertama, submit terisi menyalin brief HTML, panel hasil muncul, tautan Gmail berisi penerima dan subjek yang benar, pratinjau email tampil, serta tidak ada error console.
Bagian yang tidak dapat diverifikasi dari repositori ini adalah hasil tempel di akun Gmail sungguhan. Gmail dapat menormalkan sebagian CSS saat paste dan saat mengirim. Template karena itu memakai tabel, inline style, dan teks gelap di atas latar terang, sehingga tetap terbaca kalau sebagian dekorasi dibuang. Pengiriman tetap ditentukan aplikasi email; situs tidak menyimpan bukti penerimaan, log, ataupun penghitung.
Batasan dan kapan solusi ini perlu diganti
Alur ini menambah satu langkah: setelah menekan tombol, pengunjung membuka Gmail dan menempel brief. URL compose Gmail tidak menerima HTML, jadi tidak ada cara tanpa backend untuk langsung membuka draft berformat. Clipboard juga bisa diblokir, dan fallback teks biasa tetap tersedia untuk situasi itu.
Setiap aplikasi email juga menafsirkan isi clipboard dengan cara berbeda: sebagian mempertahankan tabel dan warna, sebagian menyederhanakan semuanya. Submit tidak disimpan di mana pun, jadi brief yang tidak dikirim pengunjung tidak meninggalkan jejak di kedua sisi. Alamat email yang dipublikasikan juga dapat dipanen seperti alamat publik lainnya.
Untuk volume saat ini, hal-hal tersebut belum menjadi alasan untuk menambahkan backend. Penggantian baru masuk akal ketika ada kebutuhan nyata: endpoint yang berfungsi jika spam atau volume membenarkannya, atau hosting sendiri yang bisa menerima POST. Sampai saat itu, teks formulir menjelaskan dengan tepat apa yang terjadi: situs ini tidak mengirim apa pun ke server.
Lokasi implementasi
Formulir tersedia di /contact/. Template email berada di src/lib/brief-email.ts, sedangkan alur clipboard, status, dan pratinjau berada di src/scripts/contact.ts dalam repositori website ini (github.com/dikaglorya/dikaglorya.github.io). Kompromi arsitekturnya dijelaskan di artikel terkait tentang struktur portofolio statis yang menaungi jurnal ini.
Catatan ini menjelaskan implementasi di repositori website ini. Tidak ada klaim tentang peringkat pencarian, benchmark performa, atau adopsi proyek.