Skip to main content

Advanced routing

Rest parameters

If the number of route segments is unknown, you can use rest syntax — for example you might implement GitHub's file viewer like so...

/[org]/[repo]/tree/[branch]/[...file]

...in which case a request for /sveltejs/kit/tree/main/documentation/docs/04-advanced-routing.md would result in the following parameters being available to the page:

{
	org: 'sveltejs',
	repo: 'kit',
	branch: 'main',
	file: 'documentation/docs/04-advanced-routing.md'
}

src/routes/a/[...rest]/z/+page.svelte will match /a/z (i.e. there's no parameter at all) as well as /a/b/z and /a/b/c/z and so on. Make sure you check that the value of the rest parameter is valid, for example using a matcher.

404 pages

Rest parameters also allow you to render custom 404s. Given these routes...

src/routes/
├ marx-brothers/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte

...the marx-brothers/+error.svelte file will not be rendered if you visit /marx-brothers/karl, because no route was matched. If you want to render the nested error page, you should create a route that matches any /marx-brothers/* request, and return a 404 from it:

src/routes/
├ marx-brothers/
| ├ [...path]/
│ ├ chico/
│ ├ harpo/
│ ├ groucho/
│ └ +error.svelte
└ +error.svelte
src/routes/marx-brothers/[...path]/+page
import { function error(status: number, body: App.Error): never (+1 overload)

Throws an error with a HTTP status code and an optional message. When called during request handling, this will cause SvelteKit to return an error response without invoking handleError. Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.

@param
status The HTTP status code. Must be in the range 400-599.
@param
body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
@throws
HttpError This error instructs SvelteKit to initiate HTTP error handling.
@throws
Error If the provided status is invalid (not between 400 and 599).
error
} from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */ export function function load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>event) {
function error(status: number, body?: {
    message: string;
} extends App.Error ? App.Error | string | undefined : never): never (+1 overload)

Throws an error with a HTTP status code and an optional message. When called during request handling, this will cause SvelteKit to return an error response without invoking handleError. Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.

@param
status The HTTP status code. Must be in the range 400-599.
@param
body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
@throws
HttpError This error instructs SvelteKit to initiate HTTP error handling.
@throws
Error If the provided status is invalid (not between 400 and 599).
error
(404, 'Not Found');
}
import { function error(status: number, body: App.Error): never (+1 overload)

Throws an error with a HTTP status code and an optional message. When called during request handling, this will cause SvelteKit to return an error response without invoking handleError. Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.

@param
status The HTTP status code. Must be in the range 400-599.
@param
body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
@throws
HttpError This error instructs SvelteKit to initiate HTTP error handling.
@throws
Error If the provided status is invalid (not between 400 and 599).
error
} from '@sveltejs/kit';
import type { type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad } from './$types'; export const const load: PageLoadload: type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>event) => {
function error(status: number, body?: {
    message: string;
} extends App.Error ? App.Error | string | undefined : never): never (+1 overload)

Throws an error with a HTTP status code and an optional message. When called during request handling, this will cause SvelteKit to return an error response without invoking handleError. Make sure you're not catching the thrown error, which would prevent SvelteKit from handling it.

@param
status The HTTP status code. Must be in the range 400-599.
@param
body An object that conforms to the App.Error type. If a string is passed, it will be used as the message property.
@throws
HttpError This error instructs SvelteKit to initiate HTTP error handling.
@throws
Error If the provided status is invalid (not between 400 and 599).
error
(404, 'Not Found');
};

If you don't handle 404 cases, they will appear in handleError

Optional parameters

A route like [lang]/home contains a parameter named lang which is required. Sometimes it's beneficial to make these parameters optional, so that in this example both home and en/home point to the same page. You can do that by wrapping the parameter in another bracket pair: [[lang]]/home

Note that an optional route parameter cannot follow a rest parameter ([...rest]/[[optional]]), since parameters are matched 'greedily' and the optional parameter would always be unused.

Matching

A route like src/routes/fruits/[page] would match /fruits/apple, but it would also match /fruits/rocketship. We don't want that. You can ensure that route parameters are well-formed by adding a matcher to your src/params.js file (or src/params.ts)...

src/params
import { function defineParams<T extends Record<string, ParamDefinition>>(definitions: T): DefinedParams<T>

Define parameter matchers for your app.

defineParams
} from '@sveltejs/kit';
export const
const params: DefinedParams<{
    fruit: (param: string) => "apple" | "orange";
}>
params
=
defineParams<{
    fruit: (param: string) => "apple" | "orange";
}>(definitions: {
    fruit: (param: string) => "apple" | "orange";
}): DefinedParams<{
    fruit: (param: string) => "apple" | "orange";
}>

Define parameter matchers for your app.

defineParams
({
fruit: (param: string) => "apple" | "orange"fruit: (param: stringparam) => { if (param: stringparam !== 'apple' && param: stringparam !== 'orange') throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Invalid fruit');
return param: "apple" | "orange"param; } });

...and augmenting your routes:

src/routes/fruits/[page=fruit]

If the pathname doesn't match, SvelteKit will try to match other routes (using the sort order specified below), before eventually returning a 404. If it does match, the returned value is passed as the param value.

You can also use a Standard Schema — for example with Valibot:

src/params
import { function defineParams<T extends Record<string, ParamDefinition>>(definitions: T): DefinedParams<T>

Define parameter matchers for your app.

defineParams
} from '@sveltejs/kit';
import * as import vv from 'valibot'; export const
const params: DefinedParams<{
    number: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.ToNumberAction<string, undefined>]>;
}>
params
=
defineParams<{
    number: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.ToNumberAction<string, undefined>]>;
}>(definitions: {
    number: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.ToNumberAction<string, undefined>]>;
}): DefinedParams<{
    number: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.ToNumberAction<string, undefined>]>;
}>

Define parameter matchers for your app.

defineParams
({
number: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.ToNumberAction<string, undefined>]>number: import vv.
pipe<v.StringSchema<undefined>, v.ToNumberAction<string, undefined>>(schema: v.StringSchema<undefined>, item1: v.ToNumberAction<string, undefined> | v.PipeAction<string, number, v.ToNumberIssue<string>>): v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.ToNumberAction<string, undefined>]> (+20 overloads)
export pipe

Adds a pipeline to a schema, that can validate and transform its input.

@param
schema The root schema.
@param
item1 The first pipe item.
@returns
A schema with a pipeline.
pipe
(import vv.
function string(): v.StringSchema<undefined> (+1 overload)
export string

Creates a string schema.

@returns
A string schema.
string
(), import vv.
toNumber<string>(): v.ToNumberAction<string, undefined> (+1 overload)
export toNumber

Creates a to number transformation action.

@returns
A to number action.
@beta
toNumber
())
});

When a schema is used, SvelteKit validates the parameter and uses the transformed output as the param value. If validation fails, the route does not match.

src/routes/items/[id=number]/+page
/** @type {import('./$types').PageLoad} */
export function function load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>load({ params: Record<string, any>

The parameters of the current page - e.g. for a route like /blog/[slug], a { slug: string } object

params
}) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
//   Error: Whoops, something bad happened
//     at [eval]:5:15
//     at Script.runInThisContext (node:vm:132:18)
//     at Object.runInThisContext (node:vm:309:38)
//     at node:internal/process/execution:77:19
//     at [eval]-wrapper:6:22
//     at evalScript (node:internal/process/execution:76:60)
//     at node:internal/main/eval_string:23:3

const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);

myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
@see
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since
v0.1.100
log
(typeof params: Record<string, any>

The parameters of the current page - e.g. for a route like /blog/[slug], a { slug: string } object

params
.id); // 'number'
}
import type { type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad } from './$types';

export const const load: PageLoadload: type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad = ({ params: Record<string, any>

The parameters of the current page - e.g. for a route like /blog/[slug], a { slug: string } object

params
}) => {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
//   Error: Whoops, something bad happened
//     at [eval]:5:15
//     at Script.runInThisContext (node:vm:132:18)
//     at Object.runInThisContext (node:vm:309:38)
//     at node:internal/process/execution:77:19
//     at [eval]-wrapper:6:22
//     at evalScript (node:internal/process/execution:76:60)
//     at node:internal/main/eval_string:23:3

const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);

myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
@see
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@since
v0.1.100
log
(typeof params: Record<string, any>

The parameters of the current page - e.g. for a route like /blog/[slug], a { slug: string } object

params
.id); // 'number'
};

Matchers run both on the server and in the browser.

Prior to SvelteKit 3, you had to define each param matcher in a separate file, all listed under a params folder (for example src/params/foo.js with export const match = (param) => param === 'foo';), and matching was determined by whether or not the matcher returns a truthy value (which means no value transformation took place).

Sorting

It's possible for multiple routes to match a given path. For example each of these routes would match /foo-abc:

src/routes/[...catchall]/+page.svelte
src/routes/[[a=x]]/+page.svelte
src/routes/[b]/+page.svelte
src/routes/foo-[c]/+page.svelte
src/routes/foo-abc/+page.svelte

SvelteKit needs to know which route is being requested. To do so, it sorts them according to the following rules...

  • More specific routes are higher priority (e.g. a route with no parameters is more specific than a route with one dynamic parameter, and so on)
  • Parameters with matchers ([name=type]) are higher priority than those without ([name])
  • [[optional]] and [...rest] parameters are ignored unless they are the final part of the route, in which case they are treated with lowest priority. In other words x/[[y]]/z is treated equivalently to x/z for the purposes of sorting
  • Ties are resolved alphabetically

...resulting in this ordering, meaning that /foo-abc will invoke src/routes/foo-abc/+page.svelte, and /foo-def will invoke src/routes/foo-[c]/+page.svelte rather than less specific routes:

src/routes/foo-abc/+page.svelte
src/routes/foo-[c]/+page.svelte
src/routes/[[a=x]]/+page.svelte
src/routes/[b]/+page.svelte
src/routes/[...catchall]/+page.svelte

Encoding

Some characters can't be used on the filesystem — / on Linux and Mac, \ / : * ? " < > | on Windows. The # and % characters have special meaning in URLs, and the [ ] ( ) characters have special meaning to SvelteKit, so these also can't be used directly as part of your route.

To use these characters in your routes, you can use hexadecimal escape sequences, which have the format [x+nn] where nn is a hexadecimal character code:

  • \[x+5c]
  • /[x+2f]
  • :[x+3a]
  • *[x+2a]
  • ?[x+3f]
  • "[x+22]
  • <[x+3c]
  • >[x+3e]
  • |[x+7c]
  • #[x+23]
  • %[x+25]
  • [[x+5b]
  • ][x+5d]
  • ([x+28]
  • )[x+29]

For example, to create a /smileys/:-) route, you would create a src/routes/smileys/[x+3a]-[x+29]/+page.svelte file.

You can determine the hexadecimal code for a character with JavaScript:

':'.String.charCodeAt(index: number): number

Returns the Unicode value of the character at the specified location.

@param
index The zero-based index of the desired character. If there is no character at the specified index, NaN is returned.
charCodeAt
(0).Number.toString(radix?: number): string

Returns a string representation of an object.

@param
radix Specifies a radix for converting numeric values to strings. This value is only used for numbers.
toString
(16); // '3a', hence '[x+3a]'

You can also use Unicode escape sequences. Generally you won't need to as you can use the unencoded character directly, but if — for some reason — you can't have a filename with an emoji in it, for example, then you can use the escaped characters. In other words, these are equivalent:

src/routes/[u+d83e][u+dd2a]/+page.svelte
src/routes/🤪/+page.svelte

The format for a Unicode escape sequence is [u+nnnn] where nnnn is a valid value between 0000 and 10ffff. (Unlike JavaScript string escaping, there's no need to use surrogate pairs to represent code points above ffff.) To learn more about Unicode encodings, consult Programming with Unicode.

Since TypeScript struggles with directories with a leading . character, you may find it useful to encode these characters when creating e.g. .well-known routes: src/routes/[x+2e]well-known/...

Advanced layouts

By default, the layout hierarchy mirrors the route hierarchy. In some cases, that might not be what you want.

(group)

Perhaps you have some routes that are 'app' routes that should have one layout (e.g. /dashboard or /item), and others that are 'marketing' routes that should have a different layout (/about or /testimonials). We can group these routes with a directory whose name is wrapped in parentheses — unlike normal directories, (app) and (marketing) do not affect the URL pathname of the routes inside them:

src/routes/
│ (app)/
│ ├ dashboard/
│ ├ item/
│ └ +layout.svelte
│ (marketing)/
│ ├ about/
│ ├ testimonials/
│ └ +layout.svelte
├ admin/
└ +layout.svelte

You can also put a +page directly inside a (group), for example if / should be an (app) or a (marketing) page.

Breaking out of layouts

The root layout applies to every page of your app — if omitted, it defaults to {@render children()}. If you want some pages to have a different layout hierarchy than the rest, then you can put your entire app inside one or more groups except the routes that should not inherit the common layouts.

In the example above, the /admin route does not inherit either the (app) or (marketing) layouts.

+page@

Pages can break out of the current layout hierarchy on a route-by-route basis. Suppose we have an /item/[id]/embed route inside the (app) group from the previous example:

src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte

Ordinarily, this would inherit the root layout, the (app) layout, the item layout and the [id] layout. We can reset to one of those layouts by appending @ followed by the segment name — or, for the root layout, the empty string. In this example, we can choose from the following options:

  • +page@[id].svelte - inherits from src/routes/(app)/item/[id]/+layout.svelte
  • +page@item.svelte - inherits from src/routes/(app)/item/+layout.svelte
  • +page@(app).svelte - inherits from src/routes/(app)/+layout.svelte
  • +page@.svelte - inherits from src/routes/+layout.svelte
src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page@(app).svelte
│ │ │ └ +layout.svelte
│ │ └ +layout.svelte
│ └ +layout.svelte
└ +layout.svelte

+layout@

Like pages, layouts can themselves break out of their parent layout hierarchy, using the same technique. For example, a +layout@.svelte component would reset the hierarchy for all its child routes.

src/routes/
├ (app)/
│ ├ item/
│ │ ├ [id]/
│ │ │ ├ embed/
│ │ │ │ └ +page.svelte  // uses (app)/item/[id]/+layout.svelte
│ │ │ ├ +layout.svelte  // inherits from (app)/item/+layout@.svelte
│ │ │ └ +page.svelte    // uses (app)/item/+layout@.svelte
│ │ └ +layout@.svelte   // inherits from root layout, skipping (app)/+layout.svelte
│ └ +layout.svelte
└ +layout.svelte

When to use layout groups

Not all use cases are suited for layout grouping, nor should you feel compelled to use them. It might be that your use case would result in complex (group) nesting, or that you don't want to introduce a (group) for a single outlier. It's perfectly fine to use other means such as composition (reusable load functions or Svelte components) or if-statements to achieve what you want. The following example shows a layout that rewinds to the root layout and reuses components and functions that other layouts can also use:

src/routes/nested/route/+layout@
<script>
	import ReusableLayout from '$lib/ReusableLayout.svelte';
	let { data, children } = $props();
</script>

<ReusableLayout {data}>
	{@render children()}
</ReusableLayout>
<script lang="ts">
	import ReusableLayout from '$lib/ReusableLayout.svelte';
	let { data, children } = $props();
</script>

<ReusableLayout {data}>
	{@render children()}
</ReusableLayout>
src/routes/nested/route/+layout
import { function reusableLoad(event: import("@sveltejs/kit").LoadEvent): Promise<Record<string, any>>reusableLoad } from '$lib/reusable-load-function';

/** @type {import('./$types').PageLoad} */
export function function load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>event) {
	// Add additional logic here, if needed
	return function reusableLoad(event: import("@sveltejs/kit").LoadEvent): Promise<Record<string, any>>reusableLoad(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>event);
}
import { function reusableLoad(event: import("@sveltejs/kit").LoadEvent): Promise<Record<string, any>>reusableLoad } from '$lib/reusable-load-function';
import type { type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad } from './$types';

export const const load: PageLoadload: type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>event) => {
	// Add additional logic here, if needed
	return function reusableLoad(event: import("@sveltejs/kit").LoadEvent): Promise<Record<string, any>>reusableLoad(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>event);
};

Further reading

Edit this page on GitHub llms.txt

previous next