This episode covers routing and navigation in SvelteKit: filesystem-based routing with +page and +layout, dynamic and nested routes, navigation with the a element and the goto function, plus route parameters, query parameters, and the preload mechanism.

A web application almost always has more than one page: home, about, product details, and so on. The way those pages are connected and data flows between them is the job of routing. In SvelteKit, routing is built from the filesystem — folders and files determine your application's URLs.
The filesystem approach lets the application structure be read directly from the folder structure. You do not need to register routes in one giant config file; just create a file and the route exists. Combined with layouts that wrap several pages at once, you can organize shared navigation and UI very cleanly.
This episode covers filesystem routing, dynamic and nested routes, navigation with the <a> element and the goto() function, plus route parameters, query parameters, and preload. This is the map that connects all the pages of your application.
Every +page.svelte file inside src/routes becomes an accessible page. The folder determines the URL:
src/routes/
├── +page.svelte # /
├── about/+page.svelte # /about
└── blog/+page.svelte # /blogsrc/routes/about/+page.svelte is rendered when a user visits /about. The plus-sign convention at the start of the filename distinguishes this file from other files in the same folder — components, API endpoints, or supporting modules do not automatically become routes.
Each page can have a +page.js file that provides data:
<script>
let { data } = $props()
</script>
<h1>Blog</h1>
{#each data.artikel as artikel}
<a href={`/blog/${artikel.slug}`}>{artikel.judul}</a>
{/each}data.artikel comes from the +page.js load function. The page renders the article list as links to the detail pages. Notice how the URL is built from artikel.slug — this is the gateway to dynamic routes in the next section.
For pages that depend on a specific identity — for example /blog/hello-svelte — use a folder with a dynamic name. The folder name becomes the parameter name:
src/routes/
└── blog
├── +page.svelte
└── [slug]/+page.svelte<script>
let { data } = $props()
</script>
<h1>{data.artikel.judul}</h1>
<p>{data.artikel.isi}</p>The [slug] folder captures a URL segment. In the load function, the parameter is available in params.slug:
export async function load({ params }) {
const artikel = await cariArtikel(params.slug)
return { artikel }
}params.slug contains the value of the URL segment. With this, a single folder serves an unlimited number of pages: /blog/satu, /blog/dua — all through the same file.
A layout is a component that wraps several pages. A +layout.svelte file in a folder applies to every route below it:
<script>
let { children } = $props()
</script>
<nav>
<a href="/blog">Semua artikel</a>
</nav>
{@render children()}{@render children()} is the Svelte 5 way to render the page content wrapped by a layout. The navbar in this layout appears on every blog page without rewriting the markup. Layouts can nest: the innermost folder's layout wraps the outer folder's layout.
The simplest and most correct way to navigate remains the <a> element. SvelteKit intercepts clicks and performs client-side navigation without a full reload:
<nav>
<a href="/">Beranda</a>
<a href="/about">Tentang</a>
<a href="/blog">Blog</a>
</nav>href="/about" is intercepted by SvelteKit for smooth navigation. As a bonus, SvelteKit prefetches links when the user hovers the pointer over them, so pages open faster. There is no special API to call — you just write ordinary links.
When navigation must be triggered from logic — after a successful form, after login, after a countdown finishes — use the goto function from $app/navigation:
<script>
import { goto } from "$app/navigation"
function selesai() {
goto("/terima-kasih")
}
</script>
<button onclick={selesai}>Selesai</button>goto("/terima-kasih") moves the user to the target route from within JavaScript code. goto also accepts options like { replaceState: true } to replace the history entry without adding a new one.
Optional data in the URL — like filters, search, or pagination — is sent as a query string. Access it from the load function via url:
export async function load({ url }) {
const halaman = Number(url.searchParams.get("halaman") ?? 1)
const q = url.searchParams.get("q") ?? ""
return { halaman, q }
}url.searchParams.get("halaman") reads the parameter from the query string. The ?? 1 combination gives a default value when the parameter is not sent. Query parameters let one page serve many variations without creating many routes.
SvelteKit runs the load function before navigation completes, so pages arrive complete with data. For applications that need extra-fast response, add data-sveltekit-preload-data to links:
<a href="/dashboard" data-sveltekit-preload-data>Dashboard</a>data-sveltekit-preload-data tells SvelteKit to fetch the page's data as soon as this attribute becomes visible — usually when the pointer approaches the link. The result: when the user actually clicks, the data is already waiting. Combine it with layouts for a navigation experience that feels instant.
Key takeaways:
+page.svelte file in src/routes determines your application's URLs via the filesystem.[slug] folder creates a dynamic route; its value is read via params in the load function.+layout.svelte wraps every page under its folder, rendering content through {@render children()}.<a> element is enough for client-side navigation; use goto() for navigation from code.url.searchParams in the load function.data-sveltekit-preload-data speeds up navigation by fetching data earlier.In the next episode 10 we will discuss forms and validation — form handling with bindings and events, validation patterns and custom validators, server-side validation in SvelteKit, and accessible forms and UX feedback. The navigation you just learned will close the form submission flow.