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:

FilePurpose
+page.svelteThe Svelte 5 component rendered for this route
+page.server.tsServer-side data loading (load) and form mutations (actions)
+layout.svelteShared UI wrapper (sidebar, header) for this route and its children
+layout.server.tsServer-side data loading shared across child routes
+server.tsAPI endpoint (GET, POST, PUT, DELETE handlers)
+error.svelteCustom 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.

RouteURLDescription
(app)/+page.svelte/Dashboard with analytics overview
(app)/users//usersUser management with CRUD, search, and export
(app)/content//contentContent management (pages list)
(app)/content/new//content/newCreate new page
(app)/content/[id]/edit//content/:id/editEdit existing page (dynamic route parameter)
(app)/analytics//analyticsCharts and data visualization
(app)/notifications//notificationsNotification center with filtering
(app)/roles//rolesRole management
(app)/database//databaseDatabase browser and stats
(app)/settings//settingsApplication 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).

RouteURLDescription
(auth)/login//loginEmail/password login + OAuth buttons
(auth)/register//registerNew account registration
(auth)/forgot-password//forgot-passwordRequest password reset token
(auth)/reset-password//reset-passwordSet new password with valid token
(auth)/lock//lockLock screen (re-enter password)
(auth)/login/google//login/googleGoogle OAuth redirect + callback
(auth)/login/github//login/githubGitHub OAuth redirect + callback

(public) — Public Marketing Pages

RouteURLDescription
(public)/pricing//pricingPricing page (no auth required)

Other Routes

RouteURLDescription
docs//docsDocumentation with its own sidebar layout
api/search//api/searchSearch endpoint for the command palette
logout//logoutServer-only logout action (no +page.svelte, just +page.server.ts)
sitemap.xml//sitemap.xmlAuto-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:

  1. Authentication check — If locals.user is null (no valid session), SvelteKit's redirect() sends a 302 to /login. No child route ever renders.
  2. Maintenance mode — If the maintenanceMode app setting is "true", non-admin users see a 503 error. Admins can still access the dashboard to manage the app.
  3. 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/search endpoint.
  • 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:enhance directive 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 the form prop.
  • Named actions — The action="?/create" attribute targets a specific named action. A single +page.server.ts can export multiple actions (create, update, delete).
  • Automatic revalidation — After a successful action, SvelteKit automatically reruns the page's load function 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