Routing
File-based routing with dynamic params, catch-all routes, route groups, and layouts.
Bosia uses file-based routing. Files in src/routes/ map directly to URLs.
Static Routes
src/routes/+page.svelte → /
src/routes/about/+page.svelte → /about
src/routes/blog/+page.svelte → /blogEach +page.svelte file becomes a page at its directory's path.
Dynamic Routes
Wrap a directory name in brackets to create a dynamic segment:
src/routes/blog/[slug]/+page.svelte → /blog/hello-world
/blog/my-post
/blog/anythingAccess the matched value via params:
// +page.server.ts
export async function load({ params }: LoadEvent) {
const post = await getPost(params.slug);
return { post };
}Inside +page.svelte and +layout.svelte, params is a top-level prop alongside data (same shape as SvelteKit):
<script lang="ts">
import type { PageProps } from "./$types";
let { data, params }: PageProps = $props();
// params.slug — string typed from the route pattern
</script>Catch-All Routes
Use [...rest] to match multiple path segments:
src/routes/all/[...catchall]/+page.svelte → /all/a
/all/a/b/c
/all/anything/hereparams.catchall contains the full matched sub-path (e.g. "a/b/c").
Route Groups
Directories wrapped in parentheses are invisible in the URL but let you share layouts:
src/routes/(public)/+layout.svelte ← shared layout
src/routes/(public)/+page.svelte → /
src/routes/(public)/about/+page.svelte → /about
src/routes/(admin)/+layout.svelte ← different layout
src/routes/(admin)/dashboard/+page.svelte → /dashboardThe (public) and (admin) groups never appear in the URL. They only control which +layout.svelte wraps the pages inside.
Route Priority
When multiple routes could match a URL, Bosia resolves them in order:
- Exact matches — static routes like
/about - Dynamic segments —
[param]routes - Catch-all —
[...rest]routes
Layouts
+layout.svelte wraps all pages and child layouts in its directory:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import "../app.css";
let { children, data } = $props();
</script>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<main>
{@render children()}
</main>Layouts nest automatically — the root layout wraps group layouts, which wrap page layouts. Child content renders where {@render children()} appears.
Layout Data
Pair a layout with +layout.server.ts to load data:
// src/routes/+layout.server.ts
import type { LoadEvent } from "bosia";
export async function load({ locals }: LoadEvent) {
return {
appName: "My App",
user: locals.user,
};
}All child pages and layouts can access this data via parent() in their own loaders.
Error Pages
Create +error.svelte to handle errors thrown by loaders:
<!-- src/routes/+error.svelte -->
<script lang="ts">
import type { ErrorProps } from "./$types";
let { error }: ErrorProps = $props();
</script>
<h1>{error.status}</h1><p>{error.message}</p>The error page receives the HttpError thrown by error() in a loader. Place it at the route level where you want to catch errors — it catches errors from all child routes. ErrorProps and the underlying PageError type come from the generated ./$types module — no manual prop typing needed.
Nested error boundaries
+error.svelte can live in any route folder, not just the root. When a loader throws, Bosia walks up from the failing route and renders the nearest +error.svelte inside the matching prefix of the layout chain — so the surrounding nav, header, and other layouts above the boundary stay visible while only the broken page is replaced.
src/routes/
+layout.svelte ← root chrome
+error.svelte ← global fallback
blog/
+layout.svelte ← blog chrome
+error.svelte ← catches errors from /blog and /blog/*
[slug]/
+page.server.ts ← if this throws, the blog +error.svelte renders
wrapped in root layout + blog layoutRules:
- An error thrown by a
+pageor+page.server.tsis caught by the deepest+error.svelteat or above the page's depth. - An error thrown by a
+layout.server.tsis caught by the deepest+error.svelteabove the failing layout — the boundary in the same folder cannot catch its own layout (it would render inside the broken layout). - If no nested boundary matches, the root
+error.svelteis used. If there is none, a plain-text response is returned. - During in-app navigation the surrounding layout stays mounted — only the broken page is swapped out, no full reload.
Page Options
Toggle rendering behavior per page by exporting flags from +page.server.ts:
// src/routes/dashboard/+page.server.ts
export const ssr = false; // skip server render, ship shell + hydrate on client
export const csr = false; // skip client hydration, server-rendered HTML only
export const prerender = true; // build to static HTML at `bosia build`
export const trailingSlash = "never"; // canonicalize URL form: "never" | "always" | "ignore"ssr = false— serverload()still runs and its result is injected as page data; the client hydrates and renders. Use for pages with browser-only deps (window, charts, third-party widgets) or auth-gated views where SSR adds latency without SEO value.csr = false— no JS shipped for the page. Static HTML only.prerender = true— captured at build time. For dynamic routes, also exportentries()returning the param values to prerender.trailingSlash— canonicalize the URL form. Defaults to"never"."never"(default) —/about/→ 308 →/about. Static export emitsabout.html."always"—/about→ 308 →/about/. Static export emitsabout/index.html."ignore"— accept both forms with no redirect. Discouraged for SEO; useful when behind a CDN that already canonicalizes.- Set on
+layout.server.tsto cascade to all child pages; child page wins on conflict. - 308 (permanent) preserves the request method, so form
POSTs submitted to the wrong slash still reach the action. - Root
/is never modified. API routes (+server.ts) are unaffected.
ssr = false together with csr = false would render nothing and is overridden to csr = true (with a dev warning). ssr = false together with prerender = true is contradictory; the route is skipped during prerender.