Home Sveltekit Overview
Post
Cancel

Sveltekit Overview

Setup

1
2
3
4
npm create svelte@latest my-app
cd my-app
npm install
npm run dev

Install the svelte plugin and use VSCode.

Concepts

What is SvelteKit?

Svelte is a component framework. SK is an app framework that does things such as routing, server side rendering, data fetching etc.

SvelteKit apps are server-rendered by default (like traditional ‘multi-page apps’ or MPAs) for excellent first load performance and SEO characteristics, but can then transition to client-side navigation (like modern ‘single-page apps’ or SPAs) to avoid jankily reloading everything (including things like third-party analytics code) when the user navigates.

Project structure

package.json lists the project’s dependencies.

svelte.config.js contains project settings.

src is where your source code codes.

src/app.html is the entry point to your app.

src/routes defines the routes of the app.

static contains any assets that should be included when your app is deployed.

Server and client

The server’s job is to turn a request into a response. Client refers to the js that loads in the browser. SvelteKit allows the two to communicate with each other. On the initial page load, the server renders the HTML, meaning content is visible as quickly as possible. The client then takes over in a process called ‘hydration’ so that subsequent navigation happen without full page reloads. Additional code and data is requested from the server as needed. All of this can be adjusted.

Routing

Pages

Routing is defined by directories in your code.

routes/+page.svelte goes to ‘/’ (homepage of your site).

routes/about/+page.svelte goes to ‘about’

Layouts

Use layouts to share UI across pages.

Define a layout in src/routes/+layout.svelte

1
2
3
4
5
6
<nav>
	<a href="/">home</a>
	<a href="/about">about</a>
</nav>

<slot />

The slot element is where the page content will be rendered.

There is nothing you have to do to apply the layout. It is automatically applied to every child route (including the sibling +page.svelte if it exists. You can nest layouts to an arbitrary depth.

Route parameters

src/routes/blog/[slug]/+page.svelte will create a route that matches /blog/one, /blog/two, /blog/three and so on.

Loading data

Page data

Every page of your app can declare a load function in a +page.sever.js file alongside +page.svelte.

  1. Create +page.server.js next to +page.svelte
  2. Create a load function and return the data you want to access in +page.svelte
  3. In +page.svelte, accept in the data as props using export let data;
  4. Whatever gets returned by the load function will automatically be assigned to data.

This is where you would do things like data fetching.

Simple example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
---
routes/blog/[slug]/+page.server.js 
---

export function load({ params }) {
    const url_slug = params.slug;
    const name = "Louie"
		// fetch other data from an API
    return {
        url_slug,
        name
    };
}

---
routes/blog/[slug]/+page.svelte
---
<script>
	export let data;
</script>

<h1>{ data.url_slug }</h1>
<h2>{ data.name }</h2>

Going to /blog/hello will show ‘hello’ and Louie.

Graphql Requests

The example below shows how to make graphql queries and load the content in svelte components.

On Directus, I created a collection called ‘bookmarks’ and one input field called title. I was running Directus locally.

First, install a library to make graphql requests:

1
npm add graphql-request graphql

Next, server side in our +page.server.js we execute the query.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
---
+page.server.js
---
import { request, gql } from 'graphql-request'

const query = gql`
  {
    bookmarks {
        id
        title
    }
  }
`

export async function load() {

    const response = await request(
        'http://127.0.0.1:8055/graphql',
        query,
      )

    return {
        response
    };
}

Then in our page, we can access the data as you would expect:

1
2
3
4
5
6
7
8
9
10
11
12
---
+page.svelte
---
<script>
	export let data;
  const bookmarks = data.response.bookmarks;
</script>

{#each bookmarks as bookmark}
    <p>{ bookmark.id }</p>
    <p>{ bookmark.title }</p>
{/each}

Layout data

+layout.svelte created UI for every child route. +layout.server.js loads data for every child route.

This means that if we have data returned in:

/routes/+layout.server.js

1
2
3
4
5
6
export function load() {
    const animal = "Dog"
    return {
        animal
    };
}

we can access it from

/routes/blog/+page.svelte

1
2
3
4
5
6
7
<script>
	export let data;
</script>

<h1>{ data.url_slug }</h1>
<h2>{ data.name }</h2>
<h3>{ data.animal }</h3>

Note that url_slug and name are still coming from /routes/blog/+page.server.js which shows that the data prop is added to from multiple places (it’s not all from either the layout exclusively or from either the +page.server.js).

The difference between page.server.js and page.js

  • +page.js/ts: Load function associated with a page. Load data first, then hand it over to the page and render the page. If page is is visited as first page, load runs on the server. If page is visited via client-side nav, load runs on the client.
  • +page.server.js/ts: Certain things are not suitable to be done in +page.js, e.g. accessing data from an API with a secret. Then you reach for this file and force execution of load to be always on the server (even during client-side nav). Can not only include load, but also form actions (released today).

Forms

The <form> element

+page.svelte

1
2
3
4
5
6
<form method="POST">
	<label>
		add a todo:
		<input name="description" />
	</label>
</form>

By default this sends a post request to the same URL as the page, but we need to create a server side action to handle the POST request.

+page.server.js

1
2
3
4
5
6
7
export const actions = {
	default: async ({ request }) => {
		const data = await request.formData();
        const description = data.get('description')
		// do something with the form data such as saving it to the database
	}
};

Name form actions

We may have multiple forms on the page, and therefore want multiple actions to handle each different submission.

+page.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form method="POST" action="?/create">
	<label>
		add a todo:
		<input name="description" />
	</label>
</form>

<ul>
	{#each data.todos as todo (todo.id)}
		<li class="todo">
			<form method="POST" action="?/delete">
				<input type="hidden" name="id" value={todo.id} />
				<button aria-label="Mark as complete"></button>
				{todo.description}
			</form>
		</li>
	{/each}
</ul>

+page.server.js

1
2
3
4
5
6
7
8
9
10
11
export const actions = {
	create: async ({ request }) => {
		const data = await request.formData();
		db.createTodo(data.get('description'));
	},

	delete: async ({ request }) => {
		const data = await request.formData();
		db.deleteTodo(data.get('id'));
	}
};

Validation

The first step is to use the browser’s built in form validation as described here. This has nothing to do with svelte.

But we can also use pass errors back from the server.js page to the svelte page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
---
+page.server.js
---
import { fail } from '@sveltejs/kit';

export function load() {...}

export const actions = {
	create: async ({ request }) => {
		const data = await request.formData();

		try {
			db.createTodo(cookies.get('userid'), data.get('description'));
		} catch (error) {
			return fail(422, {
				description: data.get('description'),
				error: error.message
			});
		}
	}

---
+page.svelte
---
<script>
	export let data;
	export let form;
</script>

<h1>todos</h1>

{#if form?.error}
	<p class="error">{form.error}</p>
{/if}

<form method="POST" action="?/create">
	<label>
		add a todo:
		<input
			name="description"
			value={form?.description ?? ''}
			required
		/>
	</label>
</form>

Progressive enhancement

If a user has js enabled (as most of the time they do) then we can use enhancement to emulate browser native behaviour except for the full page reloads between form submissions.

1
2
3
4
5
6
7
8
9
10
11
12
---
+page.svelte
---
<script>
	import { enhance } from '$app/forms';

	export let data;
	export let form;
</script>

...
<form method="POST" action="?/create" use:enhance>

API Routes

GET handlers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
---
+page.svelte
---
<script>
	/** @type {number} */
	let number;

	async function roll() {
		const response = await fetch('/roll');
		number = await response.json();
	}
</script>

<button on:click={roll}>Roll the dice</button>

{#if number !== undefined}
	<p>You rolled a {number}</p>
{/if}

---
roll/+server.js
---
import { json } from '@sveltejs/kit';

export function GET() {
	const number = Math.ceil(Math.random() * 6);

	return json(number);
}

Errors and redirects

Basics

Expected error is one that was created with the error helper:

1
2
3
4
5
6
7
8
---
src/routes/expected/+page.server.js
---
import { error } from '@sveltejs/kit';

export function load() {
	throw error(420, 'Enhance your calm');
}

An unexpected error is assumed to be a bug in your app (its message and stack trace will be logged to the console):

1
2
3
4
5
6
---
src/routes/unexpected/+page.server.js
---
export function load() {
	throw new Error('Kaboom!');
}

Error pages

To customise error pages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
src/routes/+error.svelte
---
<script>
	import { page } from '$app/stores';

	const emojis = {
		// TODO add the rest!
		420: '🫠',
		500: '💥'
	};
</script>

<h1>{$page.status} {$page.error.message}</h1>
<span style="font-size: 10em">
	{emojis[$page.status] ?? emojis[500]}
</span>

Fallback errors

If things go really wrong (i.e. while loading root layout data or while rendering the error page) then a static error page is used as a fallback.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
src/error.html
---
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>%sveltekit.error.message%</title>
		<style>
			body {
				color: #ff531a;
			}
		</style>
	</head>
	<body>
		<h1>Game over</h1>
		<p>Error code %sveltekit.status%</p>
	</body>
</html>

Redirects

You can throw redirect(...) inside load functions, form actions, API routes and the handle hook.

1
2
3
4
5
6
7
8
9
---
src/routes/a/+page.server.js
---

import { redirect } from '@sveltejs/kit';

export function load() {
	throw redirect(307, '/b');
}

Navigating to /a will take us to /b.

Page Options

Basics

You can export various page options from page.js, page.server.js, layout.js and layout.server.js:

  • ssr — whether or not pages should be server-rendered
  • csr — whether to load the SvelteKit client
  • prerender — whether to prerender pages at build time, instead of per-request
  • trailingSlash — whether to strip, add, or ignore trailing slashes in URLs

Page options can apply to individual pages (if exported from +page.js or +page.server.js), or groups of pages (if exported from +layout.js or +layout.server.js). To define an option for the whole app, export it from the root layout.

SSR

Server-side rendering is the process of generating HTML on the server and it is what SvelteKit does by default. It is good for SEO.

Some components can’t be rendered on the server, perhaps because they expect to be able to access browser globals like window immediately. If you can, you should change those components so that they can render on the server, but if you can’t then you can disable SSR:

1
2
3
4
---
src/routes/+page.server.js
---
export const ssr = false;

Note that setting ssr to false inside your root +layout.server.js effectively turns your entire app into an SPA.

CSR

Client-side rendering is what makes the page interactive, such as incrementing the counter when you click on a button.

You can disable client side rendering altogether but then it means that no JS will be served to your client and that your clients will no longer be interactive.

1
2
3
4
---
src/routes/+page.server.js
---
export const csr = false;

It is set to true by default and should generally be left as it is. But it can be a handy way of checking whether your app is usable to people who can’t use JS for whatever reason.

Prerender

Prerendering means generating HTML for a page once, at build time, rather than dynamically for each request.

The advantage is that is cheap and performant and allows you to server large numbers of users without worrying about cache-control headers. The tradeoffs is that the build process can take longer, and prerendered content can only be updated by building and deploying a new version of the application.

The basic rule is that for content to be prerenderable, any two users hitting it directly must get the same content from the server and the page must not contain form actions. Pages with dynamic route params can be prerendered as long as they are specified in the prerender.entries config or can be reached by following links that are in prerender.entries.

1
2
3
4
5
---
src/routes/+page.server.js
---

export const prerender = true;

Note that setting prerender to true inside your root +layout.server.js effectively turns SvelteKit into a static site generator (SSG).

Trailing slash

/foo is not the same as /foo/. By default Svelte skips trailing slashes meaning that a request for /foo/ will result in a redirect to /foo. If you instead want to make sure that the trailing slash is always present, you can specify the trailing slash option accordingly:

1
2
3
4
---
+page.server.js
---
export const trailingSlash = 'always';

I wouldn’t touch this - the default behaviour is fine.

Adapter

To enable the node adapter, first install it:

1
npm i -D @sveltejs/adapter-node

Then add it to svelte.config.js:

1
2
3
4
5
6
7
8
// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter()
  }
};

To build:

1
npm run build

This will create a /build directory in the root of your project. To run it locally:

1
2
cd project-root
node build

You should then be able to access it from localhost:3000.

More information here and here.

This post is licensed under CC BY 4.0 by the author.