Capstone: portal berita berbayar, ujung ke ujung
Semua yang dibahas di sebelas fase sebelumnya bertemu di sini. Kalau kamu bisa menyelesaikan ini, kamu siap memigrasikan portal yang sesungguhnya.
Intisari
- Bangun portal berita berbayar lengkap: baca, paywall, langganan, redaksi, deploy.
- Tiap kriteria penerimaan bisa diverifikasi dengan perintah, bukan dengan melihat.
- Kerjakan berurutan; tiap tahap bergantung pada yang sebelumnya.
- Yang paling penting bukan fiturnya, melainkan angka: cache hit ratio, p95, dan nol kebocoran.
- Setelah selesai, kamu punya rujukan yang bisa dipakai saat memigrasikan portal aslinya.
Yang dibangun
Portal berita dengan:
Publik
/ beranda, artikel terbaru per kategori
/berita daftar berpaginasi
/berita/[kategori] per kategori
/berita/[slug] artikel โ sebagian premium
/perusahaan tabel data, sortir & filter
/perusahaan/[kode] profil + grafik harga
/pencarian pencarian artikel
Member
/masuk /daftar /lupa-sandi
/akun profil, langganan, riwayat tagihan
/langganan pilih paket โ Midtrans Snap
Redaksi
/admin/artikel daftar, buat, sunting, terbitkan
/admin/media unggah gambar
Sistem
/api/midtrans-webhook verifikasi tanda tangan, idempoten
/api/artikel.json API publik
/sitemap-berita.xml Google News
/sitemap-[hal].xml arsip
/robots.txt
Tahap
| Tahap | Isinya | Fase |
|---|---|---|
| 1 | Skema MySQL + seed 2.000 artikel dengan panjang bervariasi | 2, 11 |
| 2 | src/lib/: db, artikel, kategori, perusahaan | 0, 2 |
| 3 | Halaman publik + layout + SEO | 1, 9 |
| 4 | Header cache + Cache-Tag | 1, 8 |
| 5 | Auth: daftar, masuk, sesi, lupa sandi | 5 |
| 6 | Paywall dengan server island + kuota terukur | 1, 5 |
| 7 | Midtrans: checkout, webhook, langganan | 6 |
| 8 | Island Vue: tabel perusahaan, grafik | 4 |
| 9 | Panel redaksi + purge cache saat terbit | 5, 8 |
| 10 | Keamanan: CSP, sanitasi, CSRF, header | 7 |
| 11 | Docker + ECS + CI/CD | 10 |
| 12 | Cloudflare: Cache Rules, WAF, kunci origin | 8 |
| 13 | Observability + uji beban | 10, 11 |
Kriteria penerimaan
Kebenaran
U=https://portal-capstone.contoh.id
# Artikel gratis tampil penuh
curl -s $U/berita/artikel-gratis | grep -c "PARAGRAF_TERAKHIR" # = 1
# Artikel premium: isi TIDAK ADA tanpa sesi
curl -s $U/berita/artikel-premium | grep -c "PARAGRAF_TERAKHIR" # = 0
# Dengan sesi member berlangganan: ADA
curl -s $U/_server-islands/KonteksPembaca?... \
-H "Cookie: __Host-sesi=$SESI_AKTIF" | grep -c "PARAGRAF_TERAKHIR" # = 1
# API JSON juga tidak membocorkan
curl -s $U/api/artikel/artikel-premium.json | grep -c "PARAGRAF_TERAKHIR" # = 0
# URL lama dialihkan 301
curl -s -o /dev/null -w '%{http_code}\n' $U/news/detail/1 # = 301
Cache
# Halaman artikel ter-cache
curl -sI $U/berita/artikel-gratis | grep -i cf-cache-status # HIT (kedua kali)
# Tidak ada Set-Cookie di halaman publik
curl -sI $U/berita/artikel-gratis | grep -ci set-cookie # = 0
# Parameter pelacakan tidak membuat entri baru
curl -sI "$U/berita/artikel-gratis?utm_source=fb" | grep -i cf-cache-status # HIT
# Server island tidak pernah di-cache
curl -sI $U/_server-islands/KonteksPembaca | grep -i cache-control # private, no-store
# Jalur akun BYPASS
curl -sI $U/akun | grep -i cf-cache-status # BYPASS
# Purge tag bekerja
# ubah judul di DB โ masih judul lama โ purge โ judul baru
Pembayaran
# Tanda tangan salah ditolak
curl -s -o /dev/null -w '%{http_code}\n' -X POST $U/api/midtrans-webhook \
-H 'Content-Type: application/json' -d '{"signature_key":"salah",...}' # = 403
# Tanda tangan benar diterima, langganan aktif
# Kirim ulang notifikasi yang SAMA 3ร โ langganan_sampai maju SEKALI
# settlement lalu pending โ status tetap aktif
# gross_amount sebagai angka, bukan string โ 403
Keamanan
# CSRF: POST dari asal asing ditolak
curl -s -o /dev/null -w '%{http_code}\n' -X POST $U/api/bookmark \
-H 'Origin: https://jahat.id' -d '{}' # = 403
# Enumerasi akun: pesan & waktu sama untuk email ada dan tidak ada
# XSS: artikel berisi <script> tidak mengeksekusinya
# SQLi: payload di ?q= tidak memakan 5 detik
# Header keamanan ada di halaman on-demand, prerender, dan aset
# Origin tidak bisa dihubungi langsung
curl -m 5 -H "Host: portal-capstone.contoh.id" https://$IP_ORIGIN/ -k # timeout/403
# Server key tidak ada di bundel klien
grep -rl "Mid-server-" dist/client/ # kosong
Performa
| Metrik | Target | Cara ukur |
|---|---|---|
| Cache hit ratio | > 90% | Cloudflare Analytics, 24 jam |
| p95 render origin | < 150 ms | Server-Timing / ALB |
| p95 server island | < 50 ms | Metrik k6 |
| JS halaman artikel | < 45 KB gzip | Skrip anggaran di CI |
| LCP | < 2,5 dtk | Data lapangan web-vitals |
| CLS | < 0,1 | Sama |
| Uji beban 2ร puncak | Nol 5xx | k6 |
| Kueri per halaman artikel | โค 4 | Penghitung kueri |
Operasional
# Deploy tanpa 5xx: jalankan k6 sambil deploy
k6 run uji/beban.js & ./deploy.sh; wait # 5xx = 0
# Rollback deploy
aws ecs update-service --task-definition <revisi-lama> # < 2 menit
# Rollback platform
# nonaktifkan Origin Rule # < 60 detik
# Cadangan bisa dipulihkan
# restore-db-instance-to-point-in-time โ verifikasi jumlah baris
# Migrasi jalan sekali meski service punya 12 task
# Tugas terjadwal jalan sekali meski dipicu bersamaan
Yang paling sering gagal
| Gagal | Kembali ke |
|---|---|
| Halaman artikel tidak ter-cache | Ada Set-Cookie atau sesi dibaca di halaman โ Fase 1, 8 |
| Isi premium bocor di HTML | Isi diambil sebelum hak dicek โ Fase 5 |
| Webhook ditolak terus | gross_amount dikonversi โ Fase 6 |
| Server island error setelah deploy | ASTRO_KEY tidak tetap โ Fase 1 |
| 502 acak | keepAliveTimeout < ALB โ Fase 10 |
| Deploy menghasilkan 5xx | Graceful shutdown atau stopTimeout โ Fase 10 |
| Task mati tanpa log | OOM โ max-old-space-size โ Fase 10 |
| Health check gagal terus | HOST bukan 0.0.0.0 โ Fase 10 |
| Member ter-logout acak | Sesi di memori proses โ Fase 5 |
| Iklan hilang setelah navigasi | astro:page-load โ Fase 4 |
Kalau kamu tersandung salah satunya, itu bukan kegagalan โ itu tujuan latihannya. Sepuluh baris di tabel ini adalah kesalahan yang jauh lebih baik ditemukan di proyek capstone daripada di portal yang melayani 1,25 juta request per jam.
Setelah selesai
| Langkah | Isinya |
|---|---|
| Tulis dokumen keputusan | Enam keputusan besarmu, dengan sinyal tinjau ulang |
| Tulis runbook | Untuk sepuluh gejala di tabel di atas |
| Ukur baseline | Cache hit, p95, biaya โ untuk dibandingkan nanti |
| Petakan ke portal aslinya | Bagian mana yang berbeda, dan kenapa |
| Mulai Fase 9 di portal asli | Strangler, dimulai dari halaman statis |
Perbedaan terbesar antara capstone dan portal aslimu bukan teknis โ melainkan data. Portal aslimu punya 200 ribu artikel dengan HTML dari tiga generasi editor, charset yang tidak konsisten, puluhan ribu URL terindeks, dan member yang sudah membayar. Fase 9 seluruhnya tentang itu, dan itu bagian yang tidak bisa dilatih di proyek bersih. Kerjakan capstone untuk menguasai mekanismenya; kerjakan inventarisasi Fase 9 untuk mengetahui apa yang sebenarnya kamu hadapi.
Latihan terakhir: selesaikan capstone-nya, lalu jalankan seluruh perintah verifikasi di halaman ini secara berurutan dan catat hasilnya. Setiap satu yang tidak sesuai target menunjuk ke fase tertentu untuk dibaca ulang. Kalau semuanya hijau, kamu punya rujukan yang bekerja โ dan portal aslimu tinggal mengikuti jalur yang sama, satu Origin Rule pada satu waktu.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.