1. Pengenalan SvelteKit
SvelteKit adalah framework full-stack resmi untuk Svelte. Dikembangkan oleh Rich Harris (pembuat Svelte dan sekarang bekerja di Vercel), SvelteKit menyediakan semua yang Anda butuhkan untuk membangun aplikasi web production-ready: routing, SSR, SSG, API routes, dan deployment ke berbagai platform.
Yang membuat SvelteKit istimewa adalah filosofinya yang berbeda dari React atau Vue: Svelte memindahkan kompilasi ke build time, sehingga bundle yang dikirim ke browser sangat kecil dan tidak memerlukan virtual DOM runtime.
Mengapa Memilih SvelteKit?
| Keunggulan | Penjelasan |
|---|---|
| Zero Bundle Size | Tidak ada virtual DOM runtime β komponen dikompilasi menjadi vanilla JS |
| Sintaks Sederhana | Svelte menggunakan sintaks yang mendekati HTML/CSS/JS biasa |
| Reactive by Default | Cukup gunakan $: untuk reactivity tanpa useState/ref |
| Form Actions | Built-in server-side form handling β bahkan berfungsi tanpa JavaScript! |
| Flexible Rendering | SSR, SSG, SPA, atau hybrid β bisa diatur per-route |
| Adapter System | Deploy ke Vercel, Netlify, Cloudflare, Node.js, atau static |
SvelteKit vs Alternatif Lain
| Aspek | SvelteKit | Next.js | Nuxt.js |
|---|---|---|---|
| UI Library | Svelte | React | Vue.js |
| Runtime Size | ~2 KB (minimal) | ~42 KB | ~33 KB |
| Reactivity | Compile-time | Runtime (hooks) | Runtime (proxy) |
| Form Handling | β Built-in actions | β Manual (Server Actions) | β Manual |
| Learning Curve | π’ Mudah | π‘ Sedang | π’ Mudah |
| Compile Approach | Ahead-of-Time | Just-in-Time | Just-in-Time |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β SVELTEKIT APPLICATION β β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β Svelte Components β β β β βββββββββββ ββββββββββββ βββββββββββββββββββ β β β β β +page β β +layout β β +error β β β β β β .svelte β β .svelte β β .svelte β β β β β ββββββ¬βββββ ββββββββββββ βββββββββββββββββββ β β β βββββββββΌββββββββββββββββββββββββββββββββββββββββββββ β β β β β βββββββββΌββββββββββββββββββββββββββββββββββββββββββββ β β β SvelteKit Core (Vite) β β β β ββββββββββββ βββββββββββββ βββββββββββββββββ β β β β β Vite β β Routing β β Adapters β β β β β β (bundler)β β (file- β β (deploy to β β β β β β β β based) β β platform) β β β β β ββββββββββββ βββββββββββββ βββββββββββββββββ β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β β Render: SSR (server) β SSG (prerender) β SPA β β Deploy: Node β Vercel β Netlify β Cloudflare β Static β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Instalasi dan Setup
Membuat Proyek Baru
# Membuat proyek SvelteKit baru npm create svelte@latest my-svelte-app # Wizard akan menanyakan: # 1. Template: Skeleton project / Demo app / Library # 2. TypeScript: Yes / No # 3. ESLint: Yes / No # 4. Prettier: Yes / No # 5. Playwright (testing): Yes / No # Masuk ke direktori cd my-svelte-app # Install dependencies npm install # Jalankan development server npm run dev # Output: # VITE v5.x ready in 300 ms # β Local: http://localhost:5173/
Struktur Proyek SvelteKit
my-svelte-app/ βββ src/ β βββ lib/ β Shared library (auto-imported $lib) β β βββ components/ β Reusable components β β β βββ Header.svelte β β β βββ Footer.svelte β β β βββ Card.svelte β β βββ server/ β Server-only code ($lib/server) β β β βββ db.js β β βββ utils/ β Utility functions β β βββ format.js β βββ routes/ β File-based routing β β βββ +page.svelte β / (home page) β β βββ +layout.svelte β Root layout β β βββ about/ β β β βββ +page.svelte β /about β β βββ blog/ β β βββ +page.svelte β /blog β β βββ [slug]/ β β βββ +page.svelte β /blog/:slug β β βββ +page.server.js β Server load + actions β β βββ +error.svelte β Error page β βββ app.html β HTML template β βββ app.d.ts β TypeScript declarations βββ static/ β Static files (tidak diproses) β βββ favicon.png βββ svelte.config.js β Konfigurasi SvelteKit βββ vite.config.js β Konfigurasi Vite βββ package.json βββ tsconfig.json
Konfigurasi SvelteKit
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Preprocess β support untuk TypeScript, PostCSS, dll
preprocess: vitePreprocess(),
kit: {
// Adapter menentukan target deployment
// adapter-auto mendeteksi platform otomatis
adapter: adapter(),
// Alias untuk import paths
alias: {
'$components': 'src/lib/components',
'$utils': 'src/lib/utils'
}
}
};
export default config;
Folder src/lib/ secara otomatis bisa diakses dengan alias $lib di seluruh proyek. Ini sangat nyaman untuk berbagi komponen dan utility functions antar halaman.
3. File-Based Routing
SvelteKit menggunakan sistem routing berbasis file yang intuitif. Setiap folder di src/routes/ menjadi route, dan file dengan nama khusus (+page.svelte, +page.server.js, dll.) menentukan perilakunya.
Konvensi Penamaan File
| File | Fungsi |
|---|---|
+page.svelte | Komponen UI yang dirender untuk route tersebut |
+page.js | Load function (universal β server + client) |
+page.server.js | Load function + form actions (server only) |
+layout.svelte | Layout wrapper untuk halaman dan child routes |
+layout.server.js | Layout load function (server only) |
+error.svelte | Halaman error untuk route tersebut |
+server.js | API endpoint (server only, no UI) |
Struktur Routing
src/routes/
βββ +page.svelte β /
βββ +layout.svelte β (root layout untuk semua halaman)
β
βββ about/
β βββ +page.svelte β /about
β
βββ blog/
β βββ +page.svelte β /blog
β βββ +page.server.js β (load data untuk /blog)
β βββ [slug]/
β βββ +page.svelte β /blog/my-post
β βββ +page.server.js β (load data per slug)
β βββ +error.svelte β (error handler)
β
βββ products/
β βββ +page.svelte β /products
β βββ [[category]]/
β βββ +page.svelte β /products atau /products/shoes
β ([[ ]] = opsional parameter)
β
βββ [...catchall]/
β βββ +page.svelte β /* (catch-all route)
β
βββ api/
βββ users/
β βββ +server.js β GET/POST /api/users
βββ auth/
βββ login/
βββ +server.js β POST /api/auth/login
Dynamic Routes
<!-- Dynamic route: /blog/:slug -->
<script>
// data di-pass dari +page.server.js load function
export let data;
</script>
<svelte:head>
<title>{data.post.title} | BeebaneLabs</title>
<meta name="description" content={data.post.excerpt} />
</svelte:head>
<article class="blog-post">
<h1>{data.post.title}</h1>
<p class="meta">
Ditulis oleh {data.post.author} Β· {data.post.date}
</p>
<div class="content">
{@html data.post.content}
</div>
</article>
<style>
.blog-post {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.meta {
color: #888;
font-size: 0.9rem;
margin-bottom: 2rem;
}
.content :global(h2) {
margin-top: 2rem;
color: var(--heading-color);
}
.content :global(p) {
line-height: 1.8;
}
</style>
Navigasi di Svelte
<script>
import { page } from '$app/stores';
</script>
<nav>
<a href="/" class:active={$page.url.pathname === '/'}>
Beranda
</a>
<a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>
Blog
</a>
<a href="/about" class:active={$page.url.pathname === '/about'}>
Tentang
</a>
<!-- Link dengan prefetching -->
<a href="/products" data-sveltekit-preload-data>
Produk
</a>
</nav>
<style>
nav {
display: flex;
gap: 1.5rem;
padding: 1rem;
}
a {
color: var(--text-color);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.2s;
}
a:hover {
background: var(--hover-bg);
}
a.active {
background: var(--primary-color);
color: white;
}
</style>
Programmatic Navigation
<script>
import { goto, invalidate, invalidateAll } from '$app/navigation';
async function handleLogin() {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
if (response.ok) {
// Redirect ke dashboard
await goto('/dashboard');
}
}
async function handleLogout() {
await fetch('/api/auth/logout', { method: 'POST' });
// Invalidate semua load data β trigger re-fetch
await invalidateAll();
// Redirect ke home
await goto('/');
}
async function refreshData() {
// Invalidate specific URL β trigger re-fetch hanya untuk URL tersebut
await invalidate('/api/products');
}
</script>
<button on:click={handleLogin}>Login</button>
<button on:click={handleLogout}>Logout</button>
<button on:click={refreshData}>Refresh Data</button>
4. Load Functions
Load functions adalah cara SvelteKit untuk mengambil data sebelum halaman dirender. Ada dua jenis: universal load (+page.js) yang berjalan di server dan client, dan server load (+page.server.js) yang hanya berjalan di server.
Server Load Function
// src/routes/blog/[slug]/+page.server.js
// Hanya berjalan di server β aman untuk database, API keys, dll
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db.js';
/** @type {import('./$types').PageServerLoad} */
export async function load({ params, setHeaders }) {
// params.slug = nilai dari URL /blog/[slug]
const { slug } = params;
// Fetch dari database
const post = await db.posts.findUnique({
where: { slug },
include: { author: true, tags: true }
});
// Jika post tidak ditemukan, throw error 404
if (!post) {
throw error(404, {
message: 'Artikel tidak ditemukan'
});
}
// Set cache headers
setHeaders({
'Cache-Control': 'public, max-age=3600'
});
// Return data β tersedia di +page.svelte sebagai `data`
return {
post: {
title: post.title,
content: post.content,
excerpt: post.excerpt,
author: post.author.name,
date: post.createdAt.toLocaleDateString('id-ID'),
tags: post.tags.map(t => t.name)
}
};
}
Universal Load Function
// src/routes/products/+page.js
// Berjalan di server (SSR) DAN client (navigasi SPA)
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, url }) {
// fetch() β SvelteKit's enhanced fetch
// Otomatis handle cookies dan relative URLs saat SSR
const category = url.searchParams.get('category') || 'all';
const page = url.searchParams.get('page') || '1';
const response = await fetch(
`/api/products?category=${category}&page=${page}`
);
const products = await response.json();
return {
products: products.items,
pagination: products.pagination,
currentCategory: category
};
}
Menggunakan Data di Komponen
<script>
// data otomatis di-pass dari load function
/** @type {import('./$types').PageData} */
export let data;
</script>
<svelte:head>
<title>{data.post.title}</title>
</svelte:head>
<article>
<h1>{data.post.title}</h1>
<p>Oleh {data.post.author} Β· {data.post.date}</p>
<div class="tags">
{#each data.post.tags as tag}
<span class="tag">{tag}</span>
{/each}
</div>
<div class="content">
{@html data.post.content}
</div>
</article>
Parent Load Data
// Akses data dari parent layout load function
export async function load({ params, parent }) {
// Ambil data dari +layout.server.js di atas
const parentData = await parent();
const currentUser = parentData.user;
const post = await db.posts.findUnique({
where: { slug: params.slug }
});
// Cek apakah user bisa edit post ini
const canEdit = currentUser?.id === post.authorId ||
currentUser?.role === 'admin';
return {
post,
canEdit
};
}
Universal load (+page.js) berjalan di server DAN client β jangan letakkan kode sensitif di sini! Server load (+page.server.js) hanya berjalan di server β aman untuk database queries, API keys, dan operasi yang membutuhkan autentikasi. Server load data secara otomatis di-strip dari data yang dikirim ke client (tidak bisa diakses hacker).
5. Form Actions
Form Actions adalah salah satu fitur terbaik SvelteKit. Ini memungkinkan Anda menangani form submissions di server tanpa perlu menulis JavaScript client-side β bahkan tetap berfungsi jika JavaScript dimatikan di browser!
Membuat Form Action
// src/routes/contact/+page.server.js
import { fail } from '@sveltejs/kit';
import { sendEmail } from '$lib/server/email.js';
/** @type {import('./$types').Actions} */
export const actions = {
// Default action β dipanggil saat form action="?/contact"
contact: async ({ request }) => {
// Ambil data dari form submission
const formData = await request.formData();
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// Validasi server-side
const errors = {};
if (!name || name.length < 2) {
errors.name = 'Nama harus minimal 2 karakter';
}
if (!email || !email.includes('@')) {
errors.email = 'Email tidak valid';
}
if (!message || message.length < 10) {
errors.message = 'Pesan harus minimal 10 karakter';
}
// Jika ada error, return data dengan fail()
if (Object.keys(errors).length > 0) {
return fail(400, {
errors,
// Preserve form values agar user tidak perlu mengetik ulang
values: { name, email, message }
});
}
// Kirim email
try {
await sendEmail({ name, email, message });
} catch (err) {
return fail(500, {
error: 'Gagal mengirim email. Coba lagi nanti.',
values: { name, email, message }
});
}
// Return success
return { success: true };
}
};
Form di Svelte Component
<script>
import { enhance } from '$app/forms';
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
let loading = false;
</script>
<svelte:head>
<title>Hubungi Kami</title>
</svelte:head>
<h1>Hubungi Kami</h1>
{#if form?.success}
<div class="alert success">
β
Pesan berhasil dikirim! Kami akan segera membalas.
</div>
{/if}
{#if form?.error}
<div class="alert error">
β {form.error}
</div>
{/if}
<!--
use:enhance = progressive enhancement
Form tetap berfungsi tanpa JS, tapi dengan JS
mendapatkan UX yang lebih baik (no full page reload)
-->
<form method="POST" action="?/contact" use:enhance={() => {
loading = true;
return async ({ result, update }) => {
loading = false;
await update();
};
}}>
<label>
Nama
<input
type="text"
name="name"
value={form?.values?.name ?? ''}
class:error={form?.errors?.name}
required
/>
{#if form?.errors?.name}
<span class="error-msg">{form.errors.name}</span>
{/if}
</label>
<label>
Email
<input
type="email"
name="email"
value={form?.values?.email ?? ''}
class:error={form?.errors?.email}
required
/>
{#if form?.errors?.email}
<span class="error-msg">{form.errors.email}</span>
{/if}
</label>
<label>
Pesan
<textarea
name="message"
rows="5"
class:error={form?.errors?.message}
required
>{form?.values?.message ?? ''}</textarea>
{#if form?.errors?.message}
<span class="error-msg">{form.errors.message}</span>
{/if}
</label>
<button type="submit" disabled={loading}>
{loading ? 'Mengirim...' : 'Kirim Pesan'}
</button>
</form>
<style>
.error-msg {
color: #ef4444;
font-size: 0.85rem;
margin-top: 4px;
}
input.error, textarea.error {
border-color: #ef4444;
}
.alert.success {
background: #065f46;
color: #a7f3d0;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.alert.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
</style>
Form actions di SvelteKit mendukung progressive enhancement. Tanpa JavaScript, form bekerja seperti form HTML biasa (full page reload). Dengan use:enhance, form mendapatkan SPA experience tanpa reload. Ini berarti aplikasi Anda tetap berfungsi untuk user dengan JavaScript yang dimatikan!
6. Server Routes (API)
File +server.js di folder routes memungkinkan Anda membuat API endpoints. Berbeda dengan +page.server.js yang terikat pada halaman, +server.js adalah standalone API routes.
Membuat API Endpoint
// src/routes/api/products/+server.js
// GET /api/products?category=shoes&page=1
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db.js';
/** @type {import('./$types').RequestHandler} */
export async function GET({ url }) {
const category = url.searchParams.get('category');
const page = parseInt(url.searchParams.get('page') || '1');
const limit = parseInt(url.searchParams.get('limit') || '20');
const where = category ? { category } : {};
const [products, total] = await Promise.all([
db.products.findMany({
where,
take: limit,
skip: (page - 1) * limit,
orderBy: { createdAt: 'desc' }
}),
db.products.count({ where })
]);
return json({
items: products,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
}
/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
const body = await request.json();
// Validasi
if (!body.name || !body.price) {
throw error(400, 'Name dan price wajib diisi');
}
const product = await db.products.create({
data: {
name: body.name,
price: parseFloat(body.price),
category: body.category || 'uncategorized',
description: body.description || ''
}
});
return json({ success: true, product }, { status: 201 });
}
Endpoint dengan Parameter
// src/routes/api/products/[id]/+server.js
// GET /api/products/123
// PUT /api/products/123
// DELETE /api/products/123
import { json, error } from '@sveltejs/kit';
export async function GET({ params }) {
const product = await db.products.findUnique({
where: { id: params.id }
});
if (!product) {
throw error(404, 'Produk tidak ditemukan');
}
return json(product);
}
export async function PUT({ params, request }) {
const body = await request.json();
const product = await db.products.update({
where: { id: params.id },
data: body
});
return json({ success: true, product });
}
export async function DELETE({ params }) {
await db.products.delete({
where: { id: params.id }
});
return json({ success: true });
}
Server-Only Code ($lib/server)
// src/lib/server/db.js
// File di $lib/server/ TIDAK BISA di-import dari client code
// Ini sangat bagus untuk menyimpan kode sensitif
import { PrismaClient } from '@prisma/client';
import { DATABASE_URL } from '$env/static/private';
const prisma = new PrismaClient({
datasources: {
db: { url: DATABASE_URL }
}
});
export { prisma as db };
// Contoh utility functions
export async function getUserFromSession(cookies) {
const sessionId = cookies.get('session_id');
if (!sessionId) return null;
const session = await prisma.session.findUnique({
where: { id: sessionId },
include: { user: true }
});
return session?.user || null;
}
7. Komponen Svelte
Svelte menggunakan pendekatan yang sangat elegan: komponen adalah file .svelte yang menggabungkan HTML, CSS, dan JavaScript dalam satu file. Tidak perlu className, tidak perlu useEffect β sintaksnya mendekati vanilla web.
Dasar Komponen Svelte
<script>
// Props β diterima dari parent
export let name;
export let email;
export let role = 'member'; // default value
// Reactive state β langsung reactive tanpa ref() atau useState()!
let isExpanded = false;
// Reactive declarations β otomatis update saat dependencies berubah
$: initials = name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();
$: badgeClass = role === 'admin' ? 'badge-admin' : 'badge-member';
// Functions
function toggleExpand() {
isExpanded = !isExpanded;
}
</script>
<div class="user-card" on:click={toggleExpand}>
<div class="avatar">{initials}</div>
<div class="info">
<h3>{name}</h3>
<p>{email}</p>
<span class="badge {badgeClass}">{role}</span>
</div>
{#if isExpanded}
<div class="details">
<slot />
</div>
{/if}
</div>
<style>
/* CSS di sini otomatis scoped ke komponen ini! */
.user-card {
padding: 1.5rem;
border-radius: 12px;
background: var(--card-bg);
border: 1px solid var(--border-color);
cursor: pointer;
transition: transform 0.2s;
}
.user-card:hover {
transform: translateY(-2px);
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: white;
}
.badge {
padding: 4px 12px;
border-radius: 999px;
font-size: 0.75rem;
}
.badge-admin { background: #ef4444; color: white; }
.badge-member { background: #3b82f6; color: white; }
.details {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
</style>
Reactivity di Svelte
<script>
// Simple reactive variable β cukup deklarasi, langsung reactive!
let count = 0;
// Reactive declaration β update otomatis saat count berubah
$: doubled = count * 2;
$: quadrupled = doubled * 2;
// Reactive statement (side effect)
$: {
console.log(`Count berubah: ${count}`);
console.log(`Doubled: ${doubled}`);
}
// Reactive if statement
$: if (count > 10) {
alert('Count sudah lebih dari 10!');
}
// Array reactivity β gunakan assignment (=) untuk trigger update
let items = ['apel', 'pisang'];
function addItem(fruit) {
// β items.push(fruit) β TIDAK reactive!
// β
items = [...items, fruit] β reactive!
items = [...items, fruit];
}
function removeItem(index) {
// β
Filter juga membuat array baru (reactive)
items = items.filter((_, i) => i !== index);
}
</script>
<!-- Template -->
<button on:click={() => count++}>
Klik: {count}
</button>
<p>Doubled: {doubled}</p>
<p>Quadrupled: {quadrupled}</p>
<ul>
{#each items as item, i}
<li>
{item}
<button on:click={() => removeItem(i)}>β</button>
</li>
{/each}
</ul>
Loops dan Conditional Rendering
<script>
let products = [
{ id: 1, name: 'Laptop', price: 15000000, inStock: true },
{ id: 2, name: 'Mouse', price: 150000, inStock: false },
{ id: 3, name: 'Keyboard', price: 500000, inStock: true }
];
let showProducts = true;
</script>
<!-- Conditional rendering -->
{#if showProducts}
<h2>Daftar Produk</h2>
<!-- Loop dengan each -->
{#each products as product (product.id)}
<div class="product">
<h3>{product.name}</h3>
<p>Rp {product.price.toLocaleString('id-ID')}</p>
<!-- Nested conditional -->
{#if product.inStock}
<span class="stock available">Tersedia</span>
{:else}
<span class="stock unavailable">Habis</span>
{/if}
</div>
{:else}
<p>Tidak ada produk ditemukan.</p>
<!-- {:else} di each = jika array kosong -->
{/each}
{:else}
<p>Produk disembunyikan.</p>
{/if}
<!-- Await blocks β handle promises langsung di template -->
{#await fetchProducts()}
<p>Loading produk...</p>
{:then products}
{#each products as product}
<p>{product.name}</p>
{/each}
{:catch error}
<p>Error: {error.message}</p>
{/await}
8. Svelte Stores
Stores di Svelte adalah reactive data containers yang bisa dibagikan antar komponen. Berbeda dengan state management library di React atau Vue, Svelte stores sangat ringan dan terintegrasi langsung ke bahasa.
Jenis-jenis Store
// src/lib/stores.js
import { writable, derived, readable } from 'svelte/store';
// 1. WRITABLE STORE β bisa dibaca dan ditulis
export const count = writable(0);
export const user = writable(null);
export const cart = writable([]);
// 2. READABLE STORE β hanya bisa dibaca (nilai dikontrol oleh store)
export const time = readable(new Date(), function start(set) {
const interval = setInterval(() => {
set(new Date());
}, 1000);
// Cleanup function β dipanggil saat tidak ada subscriber
return function stop() {
clearInterval(interval);
};
});
// 3. DERIVED STORE β dihitung dari store lain
export const formattedTime = derived(
time,
($time) => $time.toLocaleTimeString('id-ID')
);
export const cartTotal = derived(cart, ($cart) =>
$cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
export const cartCount = derived(cart, ($cart) =>
$cart.reduce((sum, item) => sum + item.quantity, 0)
);
Menggunakan Store di Komponen
<script>
import { cart, cartTotal, cartCount } from '$lib/stores.js';
function addToCart(product) {
// $cart = shorthand untuk subscribe dan set
$cart = [...$cart, { ...product, quantity: 1 }];
}
function removeFromCart(productId) {
$cart = $cart.filter(item => item.id !== productId);
}
function updateQuantity(productId, newQty) {
$cart = $cart.map(item =>
item.id === productId
? { ...item, quantity: Math.max(0, newQty) }
: item
).filter(item => item.quantity > 0);
}
// $ prefix di template = auto-subscribe/unsubscribe
// $cart = store value, $cartTotal = derived value
</script>
<div class="cart">
<h2>π Keranjang ({$cartCount} item)</h2>
{#if $cart.length === 0}
<p>Keranjang kosong</p>
{:else}
{#each $cart as item (item.id)}
<div class="cart-item">
<span>{item.name}</span>
<div class="qty-controls">
<button on:click={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
<span>{item.quantity}</span>
<button on:click={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
</div>
<span>Rp {(item.price * item.quantity).toLocaleString('id-ID')}</span>
<button on:click={() => removeFromCart(item.id)}>β</button>
</div>
{/each}
<div class="cart-total">
<strong>Total: Rp {$cartTotal.toLocaleString('id-ID')}</strong>
</div>
{/if}
</div>
9. Layouts dan Error Handling
Root Layout
<script>
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
/** @type {import('./$types').LayoutData} */
export let data;
</script>
<div class="app">
<Header user={data.user} />
<main>
<!-- <slot /> merender child routes -->
<slot />
</main>
<Footer />
</div>
<style>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main {
flex: 1;
padding: 2rem 1rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
</style>
Nested Layout
<script>
import { page } from '$app/stores';
const navItems = [
{ href: '/dashboard', label: 'Overview', icon: 'π' },
{ href: '/dashboard/products', label: 'Produk', icon: 'π¦' },
{ href: '/dashboard/orders', label: 'Pesanan', icon: 'π' },
{ href: '/dashboard/settings', label: 'Settings', icon: 'βοΈ' }
];
</script>
<div class="dashboard-layout">
<aside class="sidebar">
<h2>Dashboard</h2>
<nav>
{#each navItems as item}
<a
href={item.href}
class:active={$page.url.pathname === item.href}
>
<span>{item.icon}</span>
{item.label}
</a>
{/each}
</nav>
</aside>
<div class="dashboard-content">
<slot />
</div>
</div>
<style>
.dashboard-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
min-height: 70vh;
}
.sidebar {
background: var(--sidebar-bg);
padding: 1.5rem;
border-radius: 12px;
}
.sidebar nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 1.5rem;
}
.sidebar nav a {
padding: 0.75rem 1rem;
border-radius: 8px;
color: var(--text-color);
text-decoration: none;
transition: background 0.2s;
}
.sidebar nav a:hover { background: var(--hover-bg); }
.sidebar nav a.active { background: var(--primary-color); color: white; }
</style>
Error Handling
<script>
import { page } from '$app/stores';
</script>
<div class="error-page">
<h1>{$page.status}</h1>
{#if $page.status === 404}
<h2>Halaman Tidak Ditemukan</h2>
<p>Maaf, halaman yang Anda cari tidak tersedia.</p>
<a href="/" class="btn">Kembali ke Beranda</a>
{:else}
<h2>Terjadi Kesalahan</h2>
<p>{$page.error?.message || 'Unknown error'}</p>
<a href="/" class="btn">Kembali ke Beranda</a>
{/if}
</div>
<style>
.error-page {
text-align: center;
padding: 4rem 2rem;
}
.error-page h1 {
font-size: 6rem;
font-weight: 800;
color: var(--primary-color);
margin: 0;
}
.btn {
display: inline-block;
margin-top: 2rem;
padding: 0.75rem 2rem;
background: var(--primary-color);
color: white;
text-decoration: none;
border-radius: 8px;
}
</style>
10. Adapters dan Deployment
Adapters mengubah build output SvelteKit agar cocok dengan platform deployment target. Ini salah satu fitur paling fleksibel dari SvelteKit β satu kode bisa di-deploy ke berbagai platform.
Adapter yang Tersedia
| Adapter | Platform | Install |
|---|---|---|
| @sveltejs/adapter-auto | Auto-detect (default) | npm i @sveltejs/adapter-auto |
| @sveltejs/adapter-node | Node.js server | npm i @sveltejs/adapter-node |
| @sveltejs/adapter-static | Static files (SSG) | npm i @sveltejs/adapter-static |
| @sveltejs/adapter-vercel | Vercel (serverless) | npm i @sveltejs/adapter-vercel |
| @sveltejs/adapter-netlify | Netlify (serverless) | npm i @sveltejs/adapter-netlify |
| @sveltejs/adapter-cloudflare | Cloudflare Pages/Workers | npm i @sveltejs/adapter-cloudflare |
Static Adapter (SSG)
// svelte.config.js β untuk static site generation
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
// Default output directory
pages: 'build',
assets: 'build',
// Fallback page untuk SPA routing
fallback: '404.html',
// Precompress files dengan gzip/brotli
precompress: false,
// Strict mode β error jika ada prerender yang gagal
strict: true
})
}
};
// Di +layout.js atau +page.js, tentukan prerender:
// export const prerender = true;
Node.js Adapter
// svelte.config.js β untuk Node.js server
import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter({
// Output directory
out: 'build',
// Precompress dengan gzip
precompress: true,
// Environment prefix untuk env vars
envPrefix: ''
})
}
};
// Build: npm run build
// Jalankan: node build/index.js
// Environment variables:
// PORT=3000
// ORIGIN=https://example.com
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β SVELTEKIT ADAPTER SYSTEM β β β β SvelteKit Application β β β β β ββββββββββΌβββββββββ β β β svelte.config β β β β .js (adapter) β β β ββββββββββ¬βββββββββ β β β β β ββββββββββββ¬ββββββββΌββββββββ¬βββββββββββ β β β β β β β β β βββΌβββ βββββΌβββ ββββΌβββ βββΌββββ ββββΌβββββ β β βNodeβ βVercelβ βCF β βNet. β βStatic β β β β.js β β β βPagesβ βlify β β SSG β β β ββββββ ββββββββ βββββββ βββββββ βββββββββ β β β β Build β build/ Build β .vercel/ Build β build/ β β Run: node build/ Deploy: vercel Serve: any CDN β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
11. Quiz Pemahaman
Uji pemahaman Anda tentang SvelteKit dengan quiz interaktif berikut!
1. Apa kepanjangan dari file +page.server.js di SvelteKit?
2. Bagaimana cara membuat reactive variable di Svelte?
3. Apa fungsi dari use:enhance di form SvelteKit?
4. Apa perbedaan utama Svelte dibanding React/Vue?
5. Apa fungsi adapter di SvelteKit?
Dalam tutorial ini, Anda telah mempelajari:
- Pengenalan SvelteKit dan keunggulan compile-time approach
- Instalasi, setup, dan struktur proyek SvelteKit
- File-based routing dengan konvensi penamaan file
- Load functions (server load dan universal load)
- Form actions dengan progressive enhancement
- Server routes untuk API endpoints
- Komponen Svelte dengan reactivity dan scoped CSS
- Svelte stores (writable, readable, derived)
- Layouts dan error handling
- Adapters dan deployment ke berbagai platform