Aplikasi Form Pendaftaran Siswa dengan Astro 5 + Google Sheets
Berikut aplikasi lengkap untuk form pendaftaran siswa yang terhubung langsung ke Google Sheets. Aplikasi ini 100% gratis dengan Vercel Hobby Plan
Aplikasi PPDB ini direncanakan untuk skala 200-500 calon siswa. Karena kebutuhan teknisnya sangat kecil, sehingga sangat memungkinkan untuk menjalankan seluruh sistem ini dengan biaya Rp. 0 (selamanya) menggunakan free tier dari berbagai layanan.
Berikut adalah skenario terbaik untuk mencapai biaya nol rupiah dengan performa profesional:
1. Strategi Stack “Zero Cost”
| Komponen | Layanan | Alasan |
|---|---|---|
| Framework | Next.js atau Astro | Keduanya gratis di-host di Vercel. Astro lebih ringan jika formnya sederhana. |
| Hosting | Vercel Hobby Plan | Gratis untuk penggunaan non-komersial/skala kecil. |
| Penyimpanan | Google Sheets API atau Supabase | Google Sheets paling cocok agar admin sekolah bisa langsung melihat data tanpa dashboard tambahan. |
| Validasi | Zod | Memastikan data yang masuk tidak berantakan. |
2. Opsi Penyimpanan Terbaik untuk Sekolah
Untuk sekolah swasta, saya sangat menyarankan menggunakan Google Sheets sebagai database daripada database SQL murni. Mengapa?
- Aksesibilitas: Staf TU atau Guru bisa langsung membuka data pendaftar di Google Drive tanpa perlu akses ke database teknis.
- Tanpa Biaya: Tidak ada batasan baris (hingga jutaan sel), jauh melampaui kebutuhan 200 siswa.
- Mudah Diolah: Bisa langsung dicetak atau dibuatkan grafik laporan pendaftaran secara otomatis. Cara kerjanya:
- Buat Form di Astro.
- Gunakan Server Action untuk mengirim data.
- Gunakan library
google-auth-librarydangoogle-apisuntuk memasukkan data ke baris baru di Sheet.
3. Simulasi Keamanan & Limit (Vercel Hobby)
Dengan 200-500 pendaftar, kamu tidak akan menyentuh limit Vercel.
Berikut rinciannya:
- Serverless Execution: Vercel memberikan 100 GB-hours/bulan. Mengirim 200 form hanya akan memakan kurang dari 0,1% dari kuota ini.
- Bandwidth: Kamu dapat 100GB/bulan. Untuk form registrasi teks, ini sangat melimpah.
- Spam Protection: Karena ini gratis, kamu wajib memasang Honeypot field (input tersembunyi yang jika diisi oleh bot, form akan menolak) untuk menghindari spam yang menghabiskan kuota.
4. Estimasi Biaya Tahunan
| Item | Biaya | Keterangan |
|---|---|---|
| Hosting & Serverless | Rp 0 | Vercel Hobby Plan. |
| Database | Rp 0 | Google Sheets atau Supabase (Free Tier). |
| SSL (HTTPS) | Rp 0 | Sudah otomatis disediakan Vercel. |
| Domain | Rp 0 | Menggunakan nama-sekolah.vercel.app. |
| Total | Rp 0 | Sepenuhnya Gratis. |
Catatan: Jika ingin menggunakan domain profesional seperti .sch.id, biayanya hanya sekitar Rp 50.000 - Rp 75.000 per tahun. Ini adalah satu-satunya biaya opsional yang mungkin kamu keluarkan.
Setelah mempertimbangkan pilihan di atas. Maka kita akan mencoba yang paling mungkin dan mudah untuk diaplikasikan secara mandiri sebagai aplikasi portal pendaftaran on-line: Astro + Google Sheet
Berikut aplikasi lengkap dengan framework Astro 5 untuk form pendaftaran siswa yang terhubung langsung ke Google Sheets. Aplikasi ini 100% gratis dengan Vercel Hobby Plan.
Struktur Proyek
form-pendaftaran-siswa/├── astro.config.mjs├── tailwind.config.mjs├── package.json├── .env.example├── public/├── src/│ ├── components/│ │ └── Form.astro│ ├── layouts/│ │ └── Layout.astro│ ├── pages/│ │ └── index.astro│ └── actions/│ └── submitRegistration.js└── README.md1. Setup Proyek
Buat proyek Astro baru:
npm create astro@latest form-pendaftaran-siswa -- --template minimalcd form-pendaftaran-siswanpm install2. Install Dependensi
npm install tailwindcss @astrojs/tailwind zodnpx tailwindcss init3. Konfigurasi Astro
Buat file baru astro.config.mjs atau timpa isinya jika sudah ada:
import { defineConfig } from 'astro/config';import tailwind from '@astrojs/tailwind';
export default defineConfig({ integrations: [tailwind()], output: 'server', adapter: vercel()});Untuk menggunakan Vercel adapter, jalankan perintah di terminal:
npm install @astrojs/vercel4. Setup Google Sheets API
4.1 Buat Project di Google Cloud Console
Ikuti langkah berikut:
- Buka Google Cloud Console
- Buat project baru atau pilih yang sudah ada
- Aktifkan Google Sheets API dan Google Drive API
- Di “Credentials”, buat Service Account
- Download file JSON credentials
- Copy email Service Account (format:
xxx@xxx.iam.gserviceaccount.com)
4.2 Setup Google Sheet
- Buat Google Sheet baru di sheets.google.com
- Share sheet dengan email Service Account, beri akses Editor
- Copy ID Sheet dari URL:
https://docs.google.com/spreadsheets/d/ID_SHEET_DISINI/edit
5. File Konfigurasi Environment
Buat file .env.example di root project:
GOOGLE_SERVICE_ACCOUNT_EMAIL=your-service-account@project.iam.gserviceaccount.comGOOGLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----\nGOOGLE_SHEET_ID=your_google_sheet_id_hereCatatan:
- Untuk GOOGLE_PRIVATE_KEY, ganti
\ndengan baris baru yang sebenarnya di.env - Atau gunakan
base64encoding
6. Server Action untuk Google Sheets
Buat file src/actions/submitRegistration.js:
import { google } from 'googleapis';import { z } from 'zod';
// Schema validasi dengan Zodconst registrationSchema = z.object({ nama: z.string().min(3, "Nama minimal 3 karakter"), nisn: z.string().length(10, "NISN harus 10 digit"), tempat_lahir: z.string().min(2, "Tempat lahir wajib diisi"), tanggal_lahir: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Format tanggal: YYYY-MM-DD"), jenis_kelamin: z.enum(['L', 'P']), agama: z.string().min(1, "Agama wajib diisi"), alamat: z.string().min(10, "Alamat minimal 10 karakter"), nama_ayah: z.string().min(3, "Nama ayah minimal 3 karakter"), pekerjaan_ayah: z.string().min(2, "Pekerjaan ayah wajib diisi"), nama_ibu: z.string().min(3, "Nama ibu minimal 3 karakter"), pekerjaan_ibu: z.string().min(2, "Pekerjaan ibu wajib diisi"), no_hp: z.string().regex(/^08[0-9]{9,11}$/, "Format HP: 08xxxxxxxxxx"), email: z.string().email("Email tidak valid"), asal_sekolah: z.string().min(2, "Asal sekolah wajib diisi"), // Honeypot field untuk spam protection website: z.string().max(0, "Spam detected").optional()});
// Inisialisasi Google Sheets APIconst auth = new google.auth.GoogleAuth({ credentials: { client_email: import.meta.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, private_key: import.meta.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'), }, scopes: ['https://www.googleapis.com/auth/spreadsheets'],});
const sheets = google.sheets({ version: 'v4', auth });
export async function POST({ request }) { try { const formData = await request.formData(); const data = Object.fromEntries(formData);
// Validasi data const validatedData = registrationSchema.parse(data);
// Cek honeypot field if (data.website && data.website.length > 0) { return new Response(JSON.stringify({ success: false, message: "Spam detected" }), { status: 400 }); }
// Cek duplikasi NISN const checkResponse = await sheets.spreadsheets.values.get({ spreadsheetId: import.meta.env.GOOGLE_SHEET_ID, range: 'Sheet1!B:B', // Kolom B untuk NISN });
const existingNISNs = checkResponse.data.values?.flat() || []; if (existingNISNs.includes(validatedData.nisn)) { return new Response(JSON.stringify({ success: false, message: "NISN sudah terdaftar" }), { status: 400 }); }
// Format data untuk Google Sheets const timestamp = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
const rowData = [ timestamp, validatedData.nisn, validatedData.nama, validatedData.tempat_lahir, validatedData.tanggal_lahir, validatedData.jenis_kelamin === 'L' ? 'Laki-laki' : 'Perempuan', validatedData.agama, validatedData.alamat, validatedData.nama_ayah, validatedData.pekerjaan_ayah, validatedData.nama_ibu, validatedData.pekerjaan_ibu, validatedData.no_hp, validatedData.email, validatedData.asal_sekolah ];
// Append ke Google Sheets await sheets.spreadsheets.values.append({ spreadsheetId: import.meta.env.GOOGLE_SHEET_ID, range: 'Sheet1!A:O', valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', requestBody: { values: [rowData] } });
return new Response(JSON.stringify({ success: true, message: "Pendaftaran berhasil! Data telah tersimpan." }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) { console.error('Error:', error);
if (error instanceof z.ZodError) { return new Response(JSON.stringify({ success: false, message: "Validasi gagal", errors: error.errors.map(err => ({ field: err.path[0], message: err.message })) }), { status: 400 }); }
return new Response(JSON.stringify({ success: false, message: "Terjadi kesalahan server. Silakan coba lagi." }), { status: 500 }); }}7. Komponen Form
Buat file src/components/Form.astro:
---import { useState } from 'react';const clientSide = Astro.clientAddress();---
<div id="form-container"> <form id="registrationForm" class="space-y-6" onSubmit={async (e) => { e.preventDefault(); const form = e.target; const formData = new FormData(form); const submitBtn = form.querySelector('button[type="submit"]'); const messageDiv = document.getElementById('formMessage');
// Disable button submitBtn.disabled = true; submitBtn.innerHTML = 'Mengirim...'; messageDiv.innerHTML = ''; messageDiv.className = '';
try { const response = await fetch('/actions/submitRegistration', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) { messageDiv.innerHTML = ` <div class="p-4 rounded-md bg-green-50 border border-green-200"> <div class="flex"> <div class="flex-shrink-0"> <svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> </svg> </div> <div class="ml-3"> <p class="text-sm font-medium text-green-800">${result.message}</p> </div> </div> </div> `; form.reset(); } else { messageDiv.innerHTML = ` <div class="p-4 rounded-md bg-red-50 border border-red-200"> <div class="flex"> <div class="flex-shrink-0"> <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> </svg> </div> <div class="ml-3"> <p class="text-sm font-medium text-red-800">${result.message}</p> ${result.errors ? ` <ul class="mt-2 text-sm text-red-700 list-disc list-inside"> ${result.errors.map(err => `<li>${err.field}: ${err.message}</li>`).join('')} </ul> ` : ''} </div> </div> </div> `; } } catch (error) { messageDiv.innerHTML = ` <div class="p-4 rounded-md bg-red-50 border border-red-200"> <div class="flex"> <div class="ml-3"> <p class="text-sm font-medium text-red-800">Koneksi internet bermasalah. Silakan coba lagi.</p> </div> </div> </div> `; } finally { submitBtn.disabled = false; submitBtn.innerHTML = 'Daftar Sekarang'; } }} > <!-- Honeypot Field (Hidden from users) --> <div class="hidden" aria-hidden="true"> <label for="website">Website</label> <input type="text" id="website" name="website" tabindex="-1" autocomplete="off" /> </div>
<!-- Data Pribadi --> <div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 class="text-lg font-semibold text-gray-900 mb-4">A. Data Pribadi</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="nama" class="block text-sm font-medium text-gray-700 mb-1">Nama Lengkap *</label> <input type="text" id="nama" name="nama" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="nisn" class="block text-sm font-medium text-gray-700 mb-1">NISN (10 digit) *</label> <input type="text" id="nisn" name="nisn" required maxlength="10" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Contoh: 1234567890"> </div>
<div> <label for="tempat_lahir" class="block text-sm font-medium text-gray-700 mb-1">Tempat Lahir *</label> <input type="text" id="tempat_lahir" name="tempat_lahir" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="tanggal_lahir" class="block text-sm font-medium text-gray-700 mb-1">Tanggal Lahir *</label> <input type="date" id="tanggal_lahir" name="tanggal_lahir" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label class="block text-sm font-medium text-gray-700 mb-1">Jenis Kelamin *</label> <div class="flex space-x-4"> <label class="inline-flex items-center"> <input type="radio" name="jenis_kelamin" value="L" required class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> <span class="ml-2 text-sm text-gray-700">Laki-laki</span> </label> <label class="inline-flex items-center"> <input type="radio" name="jenis_kelamin" value="P" required class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> <span class="ml-2 text-sm text-gray-700">Perempuan</span> </label> </div> </div>
<div> <label for="agama" class="block text-sm font-medium text-gray-700 mb-1">Agama *</label> <select id="agama" name="agama" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> <option value="">Pilih Agama</option> <option value="Islam">Islam</option> <option value="Kristen">Kristen</option> <option value="Katolik">Katolik</option> <option value="Hindu">Hindu</option> <option value="Buddha">Buddha</option> <option value="Konghucu">Konghucu</option> </select> </div> </div>
<div class="mt-4"> <label for="alamat" class="block text-sm font-medium text-gray-700 mb-1">Alamat Lengkap *</label> <textarea id="alamat" name="alamat" rows="3" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Jl. Contoh No. 123, RT/RW, Kelurahan, Kecamatan, Kota"></textarea> </div> </div>
<!-- Data Orang Tua --> <div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 class="text-lg font-semibold text-gray-900 mb-4">B. Data Orang Tua</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="nama_ayah" class="block text-sm font-medium text-gray-700 mb-1">Nama Ayah *</label> <input type="text" id="nama_ayah" name="nama_ayah" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="pekerjaan_ayah" class="block text-sm font-medium text-gray-700 mb-1">Pekerjaan Ayah *</label> <input type="text" id="pekerjaan_ayah" name="pekerjaan_ayah" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="nama_ibu" class="block text-sm font-medium text-gray-700 mb-1">Nama Ibu *</label> <input type="text" id="nama_ibu" name="nama_ibu" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="pekerjaan_ibu" class="block text-sm font-medium text-gray-700 mb-1">Pekerjaan Ibu *</label> <input type="text" id="pekerjaan_ibu" name="pekerjaan_ibu" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div> </div> </div>
<!-- Kontak dan Asal Sekolah --> <div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 class="text-lg font-semibold text-gray-900 mb-4">C. Kontak & Asal Sekolah</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="no_hp" class="block text-sm font-medium text-gray-700 mb-1">No. HP (Orang Tua) *</label> <input type="tel" id="no_hp" name="no_hp" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="08xxxxxxxxxx"> </div>
<div> <label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email *</label> <input type="email" id="email" name="email" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="contoh@email.com"> </div>
<div class="md:col-span-2"> <label for="asal_sekolah" class="block text-sm font-medium text-gray-700 mb-1">Asal Sekolah (SD/MI) *</label> <input type="text" id="asal_sekolah" name="asal_sekolah" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Nama sekolah asal"> </div> </div> </div>
<!-- Submit Button --> <div class="flex justify-center"> <button type="submit" class="px-8 py-3 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"> Daftar Sekarang </button> </div>
<!-- Message Area --> <div id="formMessage" class="mt-4"></div>
<p class="text-sm text-gray-500 text-center mt-4"> * Wajib diisi<br> Data akan langsung tersimpan di sistem sekolah </p> </form></div>
<script>// Client-side validationdocument.getElementById('nisn').addEventListener('input', function(e) { this.value = this.value.replace(/[^0-9]/g, '');});
document.getElementById('no_hp').addEventListener('input', function(e) { this.value = this.value.replace(/[^0-9]/g, '');});</script>8. Halaman Utama
Buata file baru src/pages/index.astro untuk halaman depan:
---import Layout from '../layouts/Layout.astro';import Form from '../components/Form.astro';---
<Layout title="Form Pendaftaran Siswa Baru"> <div class="min-h-screen bg-gradient-to-b from-blue-50 to-white py-8"> <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <!-- Header --> <div class="text-center mb-10"> <h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-3"> FORMULIR PENDAFTARAN SISWA BARU </h1> <p class="text-lg text-gray-600 mb-2"> Tahun Ajaran 2024/2025 </p> <div class="inline-flex items-center justify-center px-4 py-2 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"> <svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> </svg> Gratis 100% - Tidak Ada Biaya Pendaftaran </div> </div>
<!-- Instructions --> <div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> <div class="flex items-start"> <svg class="h-6 w-6 text-blue-600 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/> </svg> <div> <h3 class="text-lg font-semibold text-blue-900 mb-2">Petunjuk Pengisian</h3> <ul class="text-blue-800 space-y-1"> <li>• Semua field bertanda * wajib diisi</li> <li>• Pastikan data yang diisi sesuai dengan dokumen asli</li> <li>• NISN harus 10 digit angka (dapat dicek di data Kemendikbud)</li> <li>• Data akan langsung tersimpan di sistem sekolah setelah submit</li> <li>• Tidak perlu print form, data sudah digital</li> </ul> </div> </div> </div>
<!-- Form --> <div class="bg-white rounded-xl shadow-lg p-6 md:p-8"> <Form /> </div>
<!-- Footer Note --> <div class="mt-8 text-center text-gray-500 text-sm"> <p>© 2024 [Nama Sekolah]. Semua hak dilindungi.</p> <p class="mt-1">Sistem ini berjalan di Vercel Hobby Plan - 100% Gratis</p> </div> </div> </div></Layout>9. Layout
Buat layout utama src/layouts/Layout.astro seperti ini:
---const { title } = Astro.props;---
<!DOCTYPE html><html lang="id"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="description" content="Form pendaftaran siswa baru secara online"></head><body> <slot /></body></html>10. Tailwind Config
Konfigurasi agar memakai styling modern dari Tailwind CSS dengan membuat file tailwind.config.mjs:
/** @type {import('tailwindcss').Config} */export default { content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], theme: { extend: {}, }, plugins: [],}11. Package.json
Buat file package.json untuk project.
{ "name": "form-ppdb", "type": "module", "version": "1.0.0", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/tailwind": "^5.0.4", "@astrojs/vercel": "^5.1.2", "astro": "^5.0.0", "googleapis": "^128.0.0", "tailwindcss": "^3.4.0", "zod": "^3.22.4" }}12. Deploy ke Vercel
Kita bisa men-deploy project dengan cara membuat repo di Github dan impor repo project dari Vercel. Langkah-langkahnya:
1. Push ke GitHub:
git initgit add .git commit -m "Initial commit"git branch -M maingit remote add origin https://github.com/username/repo.gitgit push -u origin main2. Deploy ke Vercel:
- Login ke vercel.com
- Import project dari GitHub
- Tambahkan environment variables:
- GOOGLE_SERVICE_ACCOUNT_EMAIL
- GOOGLE_PRIVATE_KEY
- GOOGLE_SHEET_ID
- Klik Deploy
13. Setup Google Sheets Headers
Buka Google Sheet dan buat header di baris pertama:
Timestamp | NISN | Nama Lengkap | Tempat Lahir | Tanggal Lahir | Jenis Kelamin | Agama | Alamat | Nama Ayah | Pekerjaan Ayah | Nama Ibu | Pekerjaan Ibu | No HP | Email | Asal SekolahFitur Keamanan:
- Honeypot Field: Deteksi bot otomatis
- Zod Validation: Validasi server-side yang kuat
- Duplicate Check: Cek NISN ganda
- Rate Limiting: Otomatis dari Vercel
- HTTPS: SSL gratis dari Vercel
Skala & Performa:
- 200 siswa: Hanya ~200 baris di Google Sheets
- Vercel Hobby: 100GB bandwidth/bulan cukup untuk 100,000+ submit
- Google Sheets: Gratis hingga 5 juta sel
- Zero Cost: Semuanya gratis selamanya
Aplikasi ini siap digunakan! Untuk 500 siswa bahkan lebih, sistem ini akan bekerja dengan sempurna tanpa biaya.
Dan yang paling penting adalah: admin atau TU sekolah bisa langsung melihat data dari formulir pendaftaran.