99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

(Draft) SvelteKit

(Draft) SvelteKit

This post is a draft and is still being written.
This blog is built with SvelteKit, and I plan to keep organizing the scattered related notes I gather while maintaining it.

SvelteKit is a Svelte framework that provides fast builds based on server-side rendering (SSR) and a simple routing system, along with several features that improve performance, such as SEO optimization.
In 2021, Rich Harris, the creator of Svelte, joined the Vercel team, and since then SvelteKit has become even more Vercel-friendly and easy to use.

# Installation and setup

Install a SvelteKit project with the following command.
We'll use TypeScript, ESLint, and Prettier.

BASH
1
2
3
4
5
6
# npm create svelte@latest sveltekit-project # npm create svelte@latest . npm create svelte@latest <path|project-name> ✔ Which Svelte app template? - SvelteKit demo app ✔ Add type checking with TypeScript? - Yes, using TypeScript syntax ✔ Select additional options (use arrow keys/space bar) - Select all

# SCSS

To use SCSS, install the following package.
After installing it, you can use *.scss files right away.

BASH
1
npm i -D sass
/src/app.scss
SCSS
1
2
3
4
5
6
html { --color-font: #333; body { color: var(--color-font); } }
/src/routes/+layout.svelte
SVELTE
1
2
3
4
<script lang="ts"> // ... import '../app.scss' </script>
SCSS usage example

# Path aliases

If you need a new path alias, you can use the alias option as follows.

Be careful not to overlap with the built-in $lib path alias!
You can control the $lib path alias with the config.kit.files.lib option.

/svelte.config.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ... const config = { kit: { alias: { // $lib: './src/lib', $components: './src/components', $store: './src/store' }, files: { lib: './src/lib' } } } export default config
SVELTE
1
2
3
4
<script lang="ts"> import TextField from '$components/TextField.svelte' import { scrollPaddingTop } from '$store/page' </script>

# Environment variables

/.env
1
2
3
4
CREDENTIALS=1234 API_KEY=5678 PUBLIC_CLIENT_URL=https://heropy.dev PUBLIC_DATABASE_ANON_KEY=abcd
Environment variable example

By default, environment variables can be used only on the server side.
Import them from the $env/static/private module.

TS
1
import { CREDENTIALS, API_KEY } from '$env/static/private'

Environment variables used on the client must have the PUBLIC_ prefix.
This makes it explicit that exposing the environment variable on the client is not a problem.
Import them from the $env/static/public module.

TS
1
import { PUBLIC_CLIENT_URL, PUBLIC_DATABASE_ANON_KEY } from '$env/static/public'

If needed, you can add or change the prefixes.

/svelte.config.js
JS
1
2
3
4
5
6
7
8
9
10
// ... const config = { kit: { env: { publicPrefix: 'PUBLIC_', // Default value! privatePrefix: 'SECRET_' } } } export default config
/.env
1
2
3
4
SECRET_CREDENTIALS=1234 SECRET_API_KEY=5678 PUBLIC_CLIENT_URL=https://heropy.dev PUBLIC_DATABASE_ANON_KEY=abcd
Environment variable example with an added prefix
TS
1
2
import { SECRET_CREDENTIALS, SECRET_API_KEY } from '$env/static/private' import { PUBLIC_CLIENT_URL, PUBLIC_DATABASE_ANON_KEY } from '$env/static/public'

# Font preloading

You can use font-display: swap to prevent text from not showing while the font loads (FOIT, Flash Of Invisible Text), but the system font is then swapped out after the font loads (FOUT, Flash Of Unstyled Text), so you can import the installed font and set it as a preload path as follows.

Other loading processes can be blocked and the initial load can become very slow, so preload only the main fonts the page uses!

/src/+layout.svelte
SVELTE
1
2
3
4
5
6
7
8
9
10
11
<script lang="ts"> import pretendard from '$lib/font/PretendardVariable.woff2' import '$lib/font/pretendardvariable.css' </script> <link rel="preload" href={pretendard} as="font" type="font/woff2" crossorigin="anonymous" />

Don't forget to use the font!

/src/app.css
CSS
1
2
3
4
5
body { font-family: 'Pretendard Variable', Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 'Apple SD Gothic Neo', 'Noto Sans KR', 'Malgun Gothic', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; }

SvelteKit uses the HTML <a> element for navigation, not a separate component like <Link>.
You can apply the following link options to the element itself or to a parent element.

HTML
1
<a data-sveltekit-preload-data="hover" href="/path">To Path!</a>
Option Description Value
data-sveltekit-preload-data Preload the page data hover, tap
data-sveltekit-preload-code Preload only the page code eager, viewport, hover, tap
data-sveltekit-reload Reload the page
data-sveltekit-replacestate Replace the navigation history
data-sveltekit-keepfocus Keep focus after navigation
data-sveltekit-noscroll Disable scroll reset

# Loading and passing page data

To load the data you need before rendering a page (+page.svelte), you can use +page.server.ts and +page.ts files that export a load function.
The files run in the order +page.server.ts, +page.ts, +page.svelte, and if you use all of them, you must pass the data from +page.server.ts to the +page.ts file like this.

The +page.server.ts file runs only on the server, and the +page.ts file runs on both the server and the client.

/src/routes/hello/+page.server.ts
TS
1
2
3
4
5
export async function load() { return { pageServerData: 'Hello, Server Data!' } }
1. Runs only on the server
/src/routes/hello/+page.ts
TS
1
2
3
4
5
6
export async function load({ data }) { return { ...data, // pageServerData: 'Hello, Server Data!' pageData: 'Hello, Hydration Data!' } }
2. Runs on both the server and the client
/src/routes/hello/+page.svelte
SVELTE
1
2
3
4
5
6
7
8
9
<script lang="ts"> import type { PageData } from './$types' export let data: PageData const { pageServerData, pageData } = data console.log(pageServerData) // 'Hello, Server Data!' console.log(pageData) // 'Hello, Hydration Data!' </script>
3. Renders the page

# Rerunning load functions

By default, a load function runs only once for its layout or page, but when needed, you can rerun it without reloading the browser with the depends() and invalidate() functions.

The depends() function is used inside a load function, and it declares that the load function depends on a specific identifier.
You can write the identifier freely as a '${string}:${string}' type, but it must have a : symbol in the middle.
It's a good idea to use meaningful names that clearly separate groups, states, and so on.

/src/routes/user/+page.ts
TS
1
2
3
4
5
6
7
import { getSession } from '@utils/user' export async function load({ depends }) { depends('user:session') const session = await getSession() // Session | null return { session } }

When you call the invalidate() function, the load functions that depend on that identifier run again.
For the load function in a +page.ts file, the server run is skipped and it runs only on the client.

/src/routes/user/+page.svelte
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script lang="ts"> import { invalidate } from '$app/navigation' import { signOut } from '@utils/user' import type { PageData } from './$types' export let data: PageData async function signOut() { await signOut() await invalidate('user:session') } </script> {#if data.session} <button on:click={signOut}>Sign out</button> {/if}

# Using TanStack Query

Create a query client in the root layout's load function and return it.
Then enable queries only on the client (browser), so they don't run unnecessarily on the server.

/src/routes/+layout.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
import { browser } from '$app/environment' import { QueryClient } from '@tanstack/svelte-query' export async function load() { const queryClient = new QueryClient({ defaultOptions: { queries: { enabled: browser // Enable only in the browser } } }) return { queryClient } }

Get the query client you created and connect it.

/src/routes/+layout.svelte
SVELTE
1
2
3
4
5
6
7
8
9
10
<script lang="ts"> import { QueryClientProvider } from '@tanstack/svelte-query' import type { LayoutData } from './$types' export let data: LayoutData </script> <QueryClientProvider client={data.queryClient}> <slot /> </QueryClientProvider>

When you access each page, prefetch the data you'll use with the prefetchQuery method.
Even when you visit the page again, you can use the cached data.

/src/routes/posts/[postId]/+page.ts
TS
1
2
3
4
5
6
7
8
9
10
export async function load({ parent, fetch, params }) { const { queryClient } = await parent() // Access the data returned from the parent layout (+layout.ts) const { postId } = params await queryClient.prefetchQuery({ queryKey: ['analytics', postId], queryFn: async () => (await fetch(`/api/posts/${postId}`)).json(), staleTime: 1000 * 60 * 60 * 24 // 24 hours! }) }

In each page component, create a query with the createQuery function.
The createQuery function returns a Svelte store object.

/src/routes/posts/[postId]/+page.svelte
SVELTE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script lang="ts"> import { createQuery } from '@tanstack/svelte-query' import { browser } from '$app/environment' import { page } from '$app/stores' const { postId } = $page.params const query = createQuery<ResponseValue>({ queryKey: ['analytics', postId], queryFn: async () => (await fetch(`/api/posts/${postId}`)).json(), staleTime: 1000 * 60 * 60 * 24 // 24 hours! }) query.subscribe(async ({ isFetching, data }) => { // Don't run if it's not the browser or if data is being fetched if (!browser || isFetching) return console.log(data) }) </script> <div> <div>{$query.isLoading ? 'Loading..' : $query.data}</div> </div>