SPA React/Vue/Angular #
Single Page Application (SPA) seperti React, Vue, dan Angular mengelola routing di sisi client — browser menangani navigasi tanpa reload halaman. Ini menciptakan kebutuhan khusus di sisi server: semua route yang tidak mengarah ke file fisik harus dikembalikan ke index.html agar React Router, Vue Router, atau Angular Router bisa mengambil alih.
Caddy sangat cocok untuk serve SPA karena konfigurasinya sangat simpel dan performanya sangat baik untuk static files.
Konsep: Client-Side Routing #
Masalah tanpa konfigurasi khusus:
User di /products/123 → refresh browser
Caddy cari file /products/123 → TIDAK ADA
Caddy return 404 ❌
Solusi: semua route → index.html
User di /products/123 → refresh browser
Caddy tidak temukan file → serve index.html
React Router baca URL → render komponen Products ✓
Konfigurasi Dasar SPA #
app.example.com {
root * /var/www/myapp/dist
# try_files: coba file asli, jika tidak ada → index.html
try_files {path} /index.html
file_server
}
Hanya 4 baris — ini cukup untuk semua SPA framework.
Konfigurasi Lengkap dengan Optimasi Caching #
app.example.com {
root * /var/www/myapp/dist
encode gzip zstd
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
# Aset statis dengan hash di nama file (dari build tool):
# main.a1b2c3d4.js, chunk.5e6f7g8h.css
# Cache sangat lama karena nama file berubah setiap ada perubahan kode
@hashed_assets path_regexp \.(js|css|woff2?|png|jpg|svg|ico)$
header @hashed_assets Cache-Control "public, max-age=31536000, immutable"
# index.html: JANGAN di-cache
# Browser harus selalu mengambil versi terbaru
@html path *.html /
header @html Cache-Control "no-cache, no-store, must-revalidate"
header @html Pragma "no-cache"
header @html Expires "0"
# Service worker: cache singkat
@sw path /service-worker.js /sw.js
header @sw Cache-Control "no-cache"
# Client-side routing: fallback ke index.html
try_files {path} /index.html
file_server
}
SPA + Backend API di Domain yang Sama #
example.com {
root * /var/www/myapp/dist
encode gzip zstd
# API requests ke backend
handle /api/* {
reverse_proxy api-backend:8080 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
# WebSocket (jika perlu)
handle /ws/* {
reverse_proxy ws-backend:8080
}
# Semua request lain: SPA
handle {
try_files {path} /index.html
file_server
}
}
Build dan Deploy React/Vue #
# Build React app
cd /var/www/myapp-source
npm ci
npm run build
# Output di: dist/ atau build/
# Deploy ke server
rsync -av --delete dist/ user@server:/var/www/myapp/dist/
# Atau dengan CI/CD (GitHub Actions)
# .github/workflows/deploy.yml
name: Deploy SPA
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install & Build
run: |
npm ci
npm run build
env:
VITE_API_URL: https://api.example.com
REACT_APP_API_URL: https://api.example.com
- name: Deploy to server
uses: burnett01/[email protected]
with:
switches: -avzr --delete
path: dist/
remote_path: /var/www/myapp/dist/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: deploy
remote_key: ${{ secrets.SSH_KEY }}
- name: Health check
run: |
sleep 5
curl -f https://example.com/ || exit 1
Multi-Environment dengan Caddy #
# Production
app.example.com {
root * /var/www/myapp/dist
header Content-Security-Policy "default-src 'self' https://api.example.com"
try_files {path} /index.html
file_server
}
# Staging (dilindungi basicauth)
staging.example.com {
basicauth {
dev $2a$14$devHashHere
qa $2a$14$qaHashHere
}
root * /var/www/myapp-staging/dist
# Header anti-indexing untuk staging
header X-Robots-Tag "noindex, nofollow"
try_files {path} /index.html
file_server
}
Handling Environment Variables di SPA #
SPA berjalan di browser, jadi environment variables harus di-inject saat build:
# React (create-react-app): variabel dengan prefix REACT_APP_
REACT_APP_API_URL=https://api.example.com npm run build
# Vite: variabel dengan prefix VITE_
VITE_API_URL=https://api.example.com npm run build
# Angular: gunakan environment.ts
// Atau: inject config dinamis dari server
// public/config.js (di-generate saat deployment)
window.APP_CONFIG = {
API_URL: "https://api.example.com",
VERSION: "1.2.3",
FEATURE_NEW_CHECKOUT: true
};
example.com {
root * /var/www/myapp/dist
# Jangan cache config.js
@config path /config.js
header @config Cache-Control "no-cache, no-store"
try_files {path} /index.html
file_server
}
Content Security Policy untuk SPA #
app.example.com {
root * /var/www/myapp/dist
header {
# CSP untuk SPA modern (sesuaikan dengan kebutuhan)
Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
frame-ancestors 'none';
"
}
try_files {path} /index.html
file_server
}
Preloading dan Performance Hints #
app.example.com {
root * /var/www/myapp/dist
encode gzip zstd
# Push/preload resource hints (HTTP/2 Server Push sudah deprecated,
# gunakan Link header sebagai gantinya)
@html_request {
path /
method GET
}
header @html_request Link "</assets/main.js>; rel=preload; as=script"
header @html_request Link "</assets/main.css>; rel=preload; as=style"
try_files {path} /index.html
file_server
}
Ringkasan #
try_files {path} /index.htmladalah satu-satunya konfigurasi khusus yang diperlukan untuk SPA — semua route yang tidak ada filenya akan dikembalikan keindex.html.- Set
Cache-Control: max-age=31536000, immutableuntuk file dengan hash di nama (main.a1b2c3.js) danno-cacheuntukindex.htmlagar deployment selalu mengambil versi terbaru.- Pisahkan
/api/*denganhandleblock agar API requests tidak kena fallback keindex.html.- Service worker (
/service-worker.js) harus menggunakanCache-Control: no-cache— versi lama service worker bisa menyebabkan user stuck di versi lama.- Untuk environment variables di SPA, gunakan prefix yang sesuai framework (
REACT_APP_,VITE_) saat build, atau inject via fileconfig.jsyang di-generate server-side.- Gunakan
encode gzip zstd— bundle JS/CSS modern bisa dikompresi 70-80%, membuat loading time jauh lebih cepat terutama untuk first load.
← Sebelumnya: WebSocket Berikutnya: API Gateway →
Optimasi Lighthouse Score dengan Caddy #
Google Lighthouse mengukur beberapa aspek yang bisa dioptimasi di level Caddy:
app.example.com {
root * /var/www/myapp/dist
encode gzip zstd # Lighthouse: reduce payload size
header {
# Lighthouse: HTTPS (otomatis oleh Caddy)
Strict-Transport-Security "max-age=31536000"
# Lighthouse: removes security vulnerabilities
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
# Lighthouse: proper cache policy untuk aset statis
# (dikonfigurasi per-matcher di bawah)
}
# Long cache untuk versioned assets
@versioned path_regexp \.[a-f0-9]{8,}\.(js|css|woff2?)$
header @versioned Cache-Control "public, max-age=31536000, immutable"
# No cache untuk HTML
@html path *.html /
header @html Cache-Control "no-cache"
try_files {path} /index.html
file_server
}
Preview Deployment untuk Pull Requests #
# Pattern: setiap PR mendapat subdomain preview
# pr-123.preview.example.com
*.preview.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
# Extract PR number dari subdomain
map {host} {pr_number} {
~^pr-(\d+)\.preview\. $1
default ""
}
handle {
root * /var/www/previews/pr-{pr_number}
try_files {path} /index.html
file_server
}
}
# Script deploy preview saat PR dibuat
# .github/workflows/preview.yml
# Deploy build ke /var/www/previews/pr-$PR_NUMBER/
# Caddy otomatis serve dari direktori yang sesuai
Menangani 404 yang Sesungguhnya #
Dengan try_files {path} /index.html, semua request — termasuk resource yang hilang — mengembalikan index.html dengan status 200. Untuk SEO dan analytics, mungkin kamu ingin membedakan 404 yang sesungguhnya:
app.example.com {
root * /var/www/myapp/dist
# Aset statis yang tidak ada: 404 sesungguhnya
@static_miss {
not file
path *.js *.css *.png *.jpg *.woff2 *.ico *.svg
}
respond @static_miss 404
# Route SPA: semua non-static → index.html
try_files {path} /index.html
file_server
}