· Resources

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 Form Pendaftaran Siswa dengan Astro 5 + Google Sheets

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”

KomponenLayananAlasan
FrameworkNext.js atau AstroKeduanya gratis di-host di Vercel. Astro lebih ringan jika formnya sederhana.
HostingVercel Hobby PlanGratis untuk penggunaan non-komersial/skala kecil.
PenyimpananGoogle Sheets API atau SupabaseGoogle Sheets paling cocok agar admin sekolah bisa langsung melihat data tanpa dashboard tambahan.
ValidasiZodMemastikan 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-library dan google-apis untuk 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

ItemBiayaKeterangan
Hosting & ServerlessRp 0Vercel Hobby Plan.
DatabaseRp 0Google Sheets atau Supabase (Free Tier).
SSL (HTTPS)Rp 0Sudah otomatis disediakan Vercel.
DomainRp 0Menggunakan nama-sekolah.vercel.app.
TotalRp 0Sepenuhnya 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.md

1. Setup Proyek

Buat proyek Astro baru:

Terminal window
npm create astro@latest form-pendaftaran-siswa -- --template minimal
cd form-pendaftaran-siswa
npm install

2. Install Dependensi

Terminal window
npm install tailwindcss @astrojs/tailwind zod
npx tailwindcss init

3. 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:

Terminal window
npm install @astrojs/vercel

4. Setup Google Sheets API

4.1 Buat Project di Google Cloud Console

Ikuti langkah berikut:

  1. Buka Google Cloud Console
  2. Buat project baru atau pilih yang sudah ada
  3. Aktifkan Google Sheets API dan Google Drive API
  4. Di “Credentials”, buat Service Account
  5. Download file JSON credentials
  6. Copy email Service Account (format: xxx@xxx.iam.gserviceaccount.com)

4.2 Setup Google Sheet

  1. Buat Google Sheet baru di sheets.google.com
  2. Share sheet dengan email Service Account, beri akses Editor
  3. 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.com
GOOGLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----\n
GOOGLE_SHEET_ID=your_google_sheet_id_here

Catatan:

  • Untuk GOOGLE_PRIVATE_KEY, ganti \n dengan baris baru yang sebenarnya di .env
  • Atau gunakan base64 encoding

6. Server Action untuk Google Sheets

Buat file src/actions/submitRegistration.js:

import { google } from 'googleapis';
import { z } from 'zod';
// Schema validasi dengan Zod
const 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 API
const 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 validation
document.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:

Terminal window
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/username/repo.git
git push -u origin main

2. 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 Sekolah

Fitur Keamanan:

  1. Honeypot Field: Deteksi bot otomatis
  2. Zod Validation: Validasi server-side yang kuat
  3. Duplicate Check: Cek NISN ganda
  4. Rate Limiting: Otomatis dari Vercel
  5. 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.

Komentar

Artikel terkait

Lihat semua »