Introduction
SvelteKit is an excellent framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing. At the heart of SvelteKit is a filesystem-based router. The routes of your app — i.e. the URL paths that users can access — are defined by the directories in your codebase.
In this tutorial, we are going to discuss SvelteKit routing with an awesome SvelteKit GitHub showcase built by This Dot Labs. The showcase is built with the SvelteKit starter kit on starter.dev.
We are going to tackle:
- Filesystem-based router
- +page.svelte
- +page.server
- +layout.svelte
- +layout.server
- +error.svelte
- Advanced Routing
- Rest Parameters
- (group) layouts
- Matching
Below is the current routes folder.
Prerequisites
You will need a development environment running Node.js; this tutorial was tested on Node.js version 16.18.0, and npm version 8.19.2.
Filesystem-based router
The src/routes
is the root route. You can change src/routes
to a different directory by editing the project config.
// svelte.config.js
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
routes: "src/routes", // 👈 you can change it here to anything you want
},
};
Each route directory contains one or more route files, which can be identified by their + prefix.
+page.svelte
A +page.svelte
component defines a page of your app. By default, pages are rendered both on the server (SSR) for the initial request, and in the browser (CSR) for subsequent navigation.
In the below example, we see how to render a simple login page component:
// src/routes/signin/(auth)/+page.svelte
<script>
import Auth from '$lib/components/auth/Auth.svelte';
</script>
<Auth />
+page.ts
Often, a page will need to load some data before it can be rendered. For this, we add a +page.js
(or +page.ts
, if you're TypeScript-inclined) module that exports a load function.
+page.server.ts
If your load function can only run on the server— ie, if it needs to fetch data from a database or you need to access private environment variables like API key— then you can rename +page.js
to +page.server.js
, and change the PageLoad
type to PageServerLoad
.
To pass top user repository data, and user’s gists to the client-rendered page, we do the following:
// src/routes/(authenticated)/(home)/+page.server.ts
import type { PageServerLoad } from "./$types";
import { mapUserReposToTopRepos, mapGistsToHomeGists } from "$lib/helpers";
import type {
UserGistsApiResponse,
UserReposApiResponse,
} from "$lib/interfaces";
import { ENV } from "$lib/constants/env";
export const load: PageServerLoad = async ({ fetch, parent }) => {
const repoURL = new URL("/user/repos", ENV.GITHUB_URL);
repoURL.searchParams.append("sort", "updated");
repoURL.searchParams.append("per_page", "20");
const { userInfo } = await parent();
const gistsURL = new URL(
`/users/${userInfo?.username}/gists`,
ENV.GITHUB_URL
);
try {
const reposPromise = await fetch(repoURL);
const gistsPromise = await fetch(gistsURL);
const [repoRes, gistsRes] = await Promise.all([reposPromise, gistsPromise]);
const gists = (await gistsRes.json()) as UserGistsApiResponse;
const repos = (await repoRes.json()) as UserReposApiResponse;
return {
topRepos: mapUserReposToTopRepos(repos),
gists: mapGistsToHomeGists(gists),
username: userInfo?.username,
};
} catch (err) {
console.log(err);
}
};
The page.svelte
gets access to the data by using the data variable which is of type PageServerData
.
<!-- src/routes/(authenticated)/(home)/+page.svelte -->
<script lang="ts">
import TopRepositories from '$lib/components/TopRepositories/TopRepositories.svelte';
import Gists from '$lib/components/Gists/Gists.svelte';
import type { PageServerData } from './$types';
export let data: PageServerData;
</script>
<div class="container">
<div class="page-container">
<aside>
{#if data?.gists}
<Gists gists={data.gists} />
{/if}
</aside>
{#if data?.topRepos}
<TopRepositories repos={data.topRepos} username={data?.username} />
{/if}
</div>
</div>
<style lang="scss">
@use 'src/lib/styles/variables.scss';
.page-container {
display: grid;
grid-template-columns: 1fr;
background: variables.$gray100;
@media (min-width: variables.$md) {
grid-template-columns: 24rem 1fr;
}
}
aside {
background: variables.$white;
padding: 2rem;
@media (max-width: variables.$md) {
order: 2;
}
}
</style>
+layout.svelte
As there are elements that should be visible on every page, such as top-level navigation or a footer. Instead of repeating them in every +page.svelte, we can put them in layouts.
The only requirement is that the component includes a <slot>
for the page content. For example, let's add a nav bar:
<!-- src/routes/(authenticated)/+layout.svelte -->
<script lang="ts">
import NavBar from '$lib/components/NavBar/NavBar.svelte';
import type { LayoutServerData } from './$types';
export let data: LayoutServerData;
</script>
<div class="page">
<header class="nav">
<NavBar username={data?.userInfo.username} userAvatar={data?.userInfo.avatar} />
</header>
<main class="main">
<slot /> // 👈 child routes of the layout page
</main>
</div>
+layout.server.ts
Just like +page.server.ts
, your +layout.svelte
component can get data from a load function in +layout.server.js
, and change the type from PageServerLoad
type to LayoutServerLoad.
// src/routes/(authenticated)/+layout.server.ts
import { ENV } from "$lib/constants/env";
import { remapContextUserAsync } from "$lib/helpers/context-user";
import type { LayoutServerLoad } from "./$types";
import { mapUserInfoResponseToUserInfo } from "$lib/helpers/user";
export const load: LayoutServerLoad = async ({ locals, fetch }) => {
const getContextUserUrl = new URL("/user", ENV.GITHUB_URL);
const response = await fetch(getContextUserUrl.toString());
const contextUser = await remapContextUserAsync(response);
locals.user = contextUser;
return {
userInfo: mapUserInfoResponseToUserInfo(locals.user),
};
};
+error.svelte
If an error occurs during load, SvelteKit will render a default error page. You can customize this error page on a per-route basis by adding an +error.svelte file.
In the showcase, an error.svelte
page has been added for authenticated view in case of an error.
<script>
import { page } from '$app/stores';
import ErrorMain from '$lib/components/ErrorPage/ErrorMain.svelte';
import ErrorFlash from '$lib/components/ErrorPage/ErrorFlash.svelte';
</script>
<ErrorFlash message={$page.error?.message} />
<ErrorMain status={$page.status} />
Advanced Routing
Rest Parameters
If the number of route segments is unknown, you can use spread operator syntax. This is done to implement Github’s file viewer.
/[org]/[repo]/tree/[branch]/[...file]
svelte-kit-scss.starter.dev/thisdot/starter.dev/blob/main/starters/svelte-kit-scss/README.md
would result in the following parameters being available to the page:
{
org: ‘thisdot’,
repo: 'starter.dev',
branch: 'main',
file: ‘/starters/svelte-kit-scss/README.md'
}
(group) layouts
By default, the layout hierarchy mirrors the route hierarchy. In some cases, that might not be what you want.
In the GitHub showcase, we would like an authenticated user to be able to have access to the navigation bar, error page, and user information. This is done by grouping all the relevant pages which an authenticated user can access.
Grouping can also be used to tidy your file tree and ‘group’ similar pages together for easy navigation, and understanding of the project.
Matching
In the Github showcase, we needed to have a page to show issues and pull requests for a single repo. The route src/routes/(authenticated)/[username]/[repo]/[issues]
would match /thisdot/starter.dev-github-showcases/issues
or /thisdot/starter.dev-github-showcases/pull-requests
but also /thisdot/starter.dev-github-showcases/anything
and we don't want that. You can ensure that route parameters are well-formed by adding a matcher— which takes only issues
or pull-requests
, and returns true if it is valid– to your params directory.
// src/params/issue_search_type.ts
import { IssueSearchPageTypeFiltersMap } from "$lib/constants/matchers";
import type { ParamMatcher } from "@sveltejs/kit";
export const match: ParamMatcher = (param: string): boolean => {
return Object.keys(IssueSearchPageTypeFiltersMap).includes(
param?.toLowerCase()
);
};
// src/lib/constants/matchers.ts
import { IssueSearchQueryType } from './issues-search-query-filters';
export const IssueSearchPageTypeFiltersMap = {
issues: ‘issues’,
pulls: ’pull-requests’,
};
export type IssueSearchTypePage = keyof typeof IssueSearchPageTypeFiltersMap;
...and augmenting your routes:
[issueSearchType=issue_search_type]
If the pathname doesn't match, SvelteKit will try to match other routes (using the sort order specified below), before eventually returning a 404.
Note: Matchers run both on the server and in the browser.
Conclusion
In this article, we learned about basic and advanced routing in SvelteKit by using the SvelteKit showcase example. We looked at how to work with SvelteKit's Filesystem-based router, rest parameters, and (group) layouts.
If you want to learn more about SvelteKit, please check out the SvelteKit and SCSS starter kit and the SvelteKit and SCSS GitHub showcase. All the code for our showcase project is open source. If you want to collaborate with us or have suggestions, we're always welcome to new contributions.
Thanks for reading!
If you have any questions, or run into any trouble, feel free to reach out on Twitter.