Routing & Navigation
SvelteForge Admin is built on SvelteKit's file-based routing system. Every file
inside src/routes/ automatically becomes a route in your application — no router
configuration needed. SvelteKit handles server-side rendering, client-side
navigation, data loading, and form handling out of the box for every route your Svelte 5 components define.
File-Based Routing Overview
In SvelteKit, the filesystem is the router. Each directory inside src/routes/ maps to a URL path, and special files within each directory define what happens
at that route:
| File | Purpose |
|---|---|
+page.svelte | The Svelte 5 component rendered for this route |
+page.server.ts | Server-side data loading (load) and form mutations (actions) |
+layout.svelte | Shared UI wrapper (sidebar, header) for this route and its children |
+layout.server.ts | Server-side data loading shared across child routes |
+server.ts | API endpoint (GET, POST, PUT, DELETE handlers) |
+error.svelte | Custom error page for this route subtree |
Route Groups in SvelteForge
SvelteKit supports route groups — directories wrapped in parentheses that organize routes with shared layouts without affecting the URL. SvelteForge Admin uses route groups extensively to separate concerns:
src/routes/
(app)/ ← Protected routes (dashboard, users, content...)
(auth)/ ← Public auth routes (login, register...)
(public)/ ← Public marketing pages (pricing)
docs/ ← Documentation (standalone layout)
api/ ← API endpoints
logout/ ← Standalone logout action
sitemap.xml/ ← Auto-generated sitemap The parentheses in (app), (auth), and (public) are stripped
from the URL. So src/routes/(app)/users/+page.svelte renders at /users, not /(app)/users. This is a core SvelteKit feature that lets you apply different layouts to different route groups while
keeping clean URLs.
(app) — Protected Routes
All authenticated pages live under (app)/. The layout server file acts as an auth
guard, redirecting unauthenticated users to the login page. Every Svelte 5 component in this group receives user data and notification counts from the layout.
| Route | URL | Description |
|---|---|---|
(app)/+page.svelte | / | Dashboard with analytics overview |
(app)/users/ | /users | User management with CRUD, search, and export |
(app)/content/ | /content | Content management (pages list) |
(app)/content/new/ | /content/new | Create new page |
(app)/content/[id]/edit/ | /content/:id/edit | Edit existing page (dynamic route parameter) |
(app)/analytics/ | /analytics | Charts and data visualization |
(app)/notifications/ | /notifications | Notification center with filtering |
(app)/roles/ | /roles | Role management |
(app)/database/ | /database | Database browser and stats |
(app)/settings/ | /settings | Application settings |
(auth) — Public Auth Routes
Authentication pages use a centered card layout with no sidebar. These are the only routes accessible without logging in (besides public and docs pages).
| Route | URL | Description |
|---|---|---|
(auth)/login/ | /login | Email/password login + OAuth buttons |
(auth)/register/ | /register | New account registration |
(auth)/forgot-password/ | /forgot-password | Request password reset token |
(auth)/reset-password/ | /reset-password | Set new password with valid token |
(auth)/lock/ | /lock | Lock screen (re-enter password) |
(auth)/login/google/ | /login/google | Google OAuth redirect + callback |
(auth)/login/github/ | /login/github | GitHub OAuth redirect + callback |
(public) — Public Marketing Pages
| Route | URL | Description |
|---|---|---|
(public)/pricing/ | /pricing | Pricing page (no auth required) |
Other Routes
| Route | URL | Description |
|---|---|---|
docs/ | /docs | Documentation with its own sidebar layout |
api/search/ | /api/search | Search endpoint for the command palette |
logout/ | /logout | Server-only logout action (no +page.svelte, just +page.server.ts) |
sitemap.xml/ | /sitemap.xml | Auto-generated XML sitemap |
Auth Guard Deep Dive
The auth guard is implemented in (app)/+layout.server.ts. Because SvelteKit runs layout server loads before page server loads, this single file
protects every route in the (app) group.
// src/routes/(app)/+layout.server.ts
import { redirect, error } from "@sveltejs/kit";
import { db } from "#lib/server/db/index.js";
import { notifications, appSettings } from "#lib/server/db/schema.js";
import { eq, and, or, isNull, sql, desc } from "drizzle-orm";
import type { LayoutServerLoad } from "./$types.js";
export const load: LayoutServerLoad = async ({ locals }) => {
// Auth check — redirect to login if not authenticated
if (!locals.user) {
redirect(302, "/login");
}
// Maintenance mode — block non-admins with 503
const maintenanceSetting = await db.query.appSettings.findFirst({
where: eq(appSettings.key, "maintenanceMode"),
});
if (maintenanceSetting?.value === "true" && locals.user.role !== "admin") {
error(503, "The application is currently under maintenance.");
}
// Load sidebar data: unread count + recent notifications
const userNotificationFilter = or(
eq(notifications.userId, locals.user.id),
isNull(notifications.userId)
);
const [countResult] = await db
.select({ count: sql<number>`count(*)` })
.from(notifications)
.where(and(eq(notifications.read, false), userNotificationFilter));
const recentNotifications = await db
.select({
id: notifications.id,
title: notifications.title,
message: notifications.message,
type: notifications.type,
createdAt: notifications.createdAt,
})
.from(notifications)
.where(and(eq(notifications.read, false), userNotificationFilter))
.orderBy(desc(notifications.createdAt))
.limit(5);
return {
user: locals.user,
unreadNotificationCount: countResult?.count ?? 0,
recentNotifications,
};
}; This layout server load does three things:
- Authentication check — If
locals.useris null (no valid session), SvelteKit'sredirect()sends a 302 to/login. No child route ever renders. - Maintenance mode — If the
maintenanceModeapp setting is "true", non-admin users see a 503 error. Admins can still access the dashboard to manage the app. - Sidebar data — Loads unread notification count and the 5 most recent unread
notifications. This data is available to every Svelte 5 component in the
(app)group via$page.data.
Server Hooks
SvelteKit's hooks.server.ts runs on every request before
any route handler. SvelteForge uses it to validate sessions and populate event.locals:
// src/hooks.server.ts
import {
validateSession,
setSessionCookie,
deleteSessionCookie,
SESSION_COOKIE_NAME,
} from "#lib/server/auth.js";
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get(SESSION_COOKIE_NAME);
if (!token) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await validateSession(token);
if (session) {
// Refresh cookie and update session metadata (UA, IP)
setSessionCookie(event.cookies, token, session.expiresAt);
// ... update userAgent and ipAddress in DB
} else {
deleteSessionCookie(event.cookies);
}
event.locals.user = user;
event.locals.session = session;
return resolve(event);
}; After the hook runs, every SvelteKit server route (load functions, actions, API
handlers) can access event.locals.user and event.locals.session. These
types are defined in src/app.d.ts:
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user: SessionUser | null;
session: Session | null;
}
}
} Layout Hierarchy
SvelteKit nests layouts automatically. Each route group has its own layout that wraps all pages within it. SvelteForge Admin has four distinct layout trees:
Root Layout
+layout.svelte at the root of src/routes/. Minimal — it simply renders
its children. All global providers (fonts, CSS resets, theme) are applied here.
App Layout — (app)/+layout.svelte
The main application shell for authenticated users. This Svelte 5 component renders:
- Sidebar (
app-sidebar.svelte) — Navigation links, user avatar, collapsible on mobile. - Top bar — Breadcrumb navigation, search, notification bell, theme toggle.
- Command palette — Keyboard-driven search (
Ctrl+K/Cmd+K) that queries the/api/searchendpoint. - Notification bell — Shows unread count with recent notification dropdown.
- Theme toggle — Dark/light mode via
mode-watcher.
Auth Layout — (auth)/+layout.svelte
A centered card layout for login, register, and password reset pages. No sidebar or top bar — just a clean, focused form in the center of the screen.
Docs Layout — docs/+layout.svelte
This documentation layout provides its own sidebar navigation with section groupings (Getting
Started, Core Concepts, Features, Advanced) and prose styling for article content. It is separate
from the (app) layout and does not require authentication.
Breadcrumb Navigation
The app layout auto-generates breadcrumbs from the current URL pathname. This is implemented
directly in the Svelte 5 component using $derived:
<script lang="ts">
import { page } from "$app/state";
let segments = $derived(
page.url.pathname
.split("/")
.filter(Boolean)
.map((s) => ({
label: s.charAt(0).toUpperCase() + s.slice(1),
href: "/" + s,
}))
);
</script> - The path is split into segments and each segment is capitalized.
- The root path (
/) shows "Dashboard" as the breadcrumb. - Each breadcrumb links to its URL segment for quick navigation.
Adding New Routes
Adding a new protected page to SvelteForge Admin takes four steps. Because SvelteKit's file-based routing and the (app) layout guard work together,
your new route is automatically authenticated and styled.
Step 1: Create the Route Directory
mkdir -p src/routes/\(app\)/reports Step 2: Add the Server Load Function
// src/routes/(app)/reports/+page.server.ts
import { db } from "#lib/server/db/index.js";
import { pages } from "#lib/server/db/schema.js";
import { sql } from "drizzle-orm";
import type { PageServerLoad } from "./$types.js";
export const load: PageServerLoad = async () => {
const stats = await db
.select({
status: pages.status,
count: sql<number>`count(*)`,
})
.from(pages)
.groupBy(pages.status);
return { stats };
}; Step 3: Add the Svelte 5 Page Component
<!-- src/routes/(app)/reports/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<div class="space-y-6">
<h1 class="text-2xl font-bold">Reports</h1>
<div class="grid gap-4 md:grid-cols-3">
{#each data.stats as stat}
<div class="rounded-lg border p-4">
<p class="text-muted-foreground text-sm">{stat.status}</p>
<p class="text-3xl font-bold">{stat.count}</p>
</div>
{/each}
</div>
</div> Step 4: Add Sidebar Link
Add a navigation entry in src/lib/components/app-sidebar.svelte. The auth guard in (app)/+layout.server.ts automatically protects the new route — no additional configuration
needed.
Form Actions
SvelteKit form actions handle server-side mutations (create, update, delete) with built-in progressive enhancement. SvelteForge Admin uses them throughout — here is a pattern from the user management page:
Server-Side Action
// src/routes/(app)/users/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types.js";
export const actions: Actions = {
create: async ({ request }) => {
const formData = await request.formData();
const name = formData.get("name");
const email = formData.get("email");
const username = formData.get("username");
const password = formData.get("password");
const role = formData.get("role");
// Server-side validation
if (typeof name !== "string" || name.length < 1 || name.length > 100) {
return fail(400, { message: "Name is required (1-100 characters)" });
}
if (typeof email !== "string" || !email.includes("@")) {
return fail(400, { message: "Valid email is required" });
}
// ... hash password, insert into database
await db.insert(users).values({
id: generateId(10),
email: email.toLowerCase(),
username: username.toLowerCase(),
passwordHash,
name,
role: role as "admin" | "editor" | "viewer",
});
return { success: true };
},
}; Svelte 5 Form Component
<script lang="ts">
import { enhance } from "$app/forms";
let { form } = $props();
</script>
<form method="POST" action="?/create" use:enhance>
<input name="name" required />
<input name="email" type="email" required />
<input name="username" required />
<input name="password" type="password" required />
<select name="role">
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Create User</button>
{#if form?.message}
<p class="text-destructive">{form.message}</p>
{/if}
</form> Key points about SvelteKit form actions:
- Progressive enhancement — The
use:enhancedirective makes forms submit via fetch with no page reload, but they still work without JavaScript enabled. - Server-side validation — Always validate on the server. The
fail()function returns error data to the Svelte 5 component via theformprop. - Named actions — The
action="?/create"attribute targets a specific named action. A single+page.server.tscan export multiple actions (create, update, delete). - Automatic revalidation — After a successful action, SvelteKit automatically reruns the page's
loadfunction and updates the Svelte 5 component with fresh data.
API Routes
SvelteKit API routes are defined in +server.ts files and export HTTP method
handlers.
/api/search — Command Palette Search
The search endpoint powers the command palette (Ctrl+K). It queries users, pages, and
notifications in parallel using Promise.all:
// src/routes/api/search/+server.ts
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types.js";
export const GET: RequestHandler = async ({ url, locals }) => {
if (!locals.user) {
error(401, "Unauthorized");
}
const q = url.searchParams.get("q")?.trim() ?? "";
if (q.length < 2) return Response.json([]);
const pattern = `%${q}%`;
const [userResults, pageResults, notificationResults] =
await Promise.all([
db.select({ id: users.id, name: users.name, email: users.email })
.from(users)
.where(or(sql`${users.name} LIKE ${pattern}`, sql`${users.email} LIKE ${pattern}`))
.limit(5),
db.select({ id: pages.id, title: pages.title, slug: pages.slug })
.from(pages)
.where(sql`${pages.title} LIKE ${pattern}`)
.limit(5),
// ... notifications query
]);
return Response.json(results);
}; /sitemap.xml — Auto-Generated Sitemap
The sitemap endpoint generates XML dynamically from the application's routes. It is served at /sitemap.xml — SvelteKit treats directories named with file extensions
as valid routes.
Page Transitions
SvelteForge Admin uses the View Transitions API for smooth cross-fade animations
when navigating between pages. SvelteKit has built-in support for view
transitions — you can enable them by adding the onNavigate lifecycle hook:
<script lang="ts">
import { onNavigate } from "$app/navigation";
onNavigate((navigation) => {
if (!document.startViewTransition) return;
return new Promise((resolve) => {
document.startViewTransition(async () => {
resolve();
await navigation.complete;
});
});
});
</script> This provides a smooth cross-fade between pages in browsers that support the View Transitions API, while falling back gracefully to instant navigation in older browsers.
Summary
SvelteKit's routing system gives SvelteForge Admin a clean, convention-based architecture:
- Route groups (
(app),(auth),(public)) separate concerns with different layouts. - Layout server loads provide auth guards and shared data to all child Svelte 5 components.
- Server hooks validate sessions on every request before any route handler runs.
- Form actions handle mutations with progressive enhancement and server-side validation.
- API routes power features like the command palette search.
- Adding a new page is as simple as creating files in the right directory — SvelteKit handles the rest.
Need More?
Go Premium with DashboardPack
SvelteForge Admin demonstrates SvelteKit's routing capabilities with a handful of well-crafted pages. DashboardPack premium templates take it further with 50+ pages, nested route hierarchies, multi-step wizards, tabbed interfaces, and advanced Svelte 5 component patterns — all built on SvelteKit.
- Apex (Svelte) — 39 pages with 6 dashboards, CRUD modules, and SvelteKit file-based routing patterns
- Zenith — Advanced analytics with dynamic route parameters and drill-down views
- Signal — Real-time monitoring with streaming data via SvelteKit server-sent events