(Draft) SvelteKit
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.
123456# 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.
1npm i -D sass
123456html { --color-font: #333; body { color: var(--color-font); } }
1234<script lang="ts"> // ... import '../app.scss' </script>
# Path aliases
If you need a new path alias, you can use the alias option as follows.
1234567891011121314// ... const config = { kit: { alias: { // $lib: './src/lib', $components: './src/components', $store: './src/store' }, files: { lib: './src/lib' } } } export default config
1234<script lang="ts"> import TextField from '$components/TextField.svelte' import { scrollPaddingTop } from '$store/page' </script>
# Environment variables
1234CREDENTIALS=1234 API_KEY=5678 PUBLIC_CLIENT_URL=https://heropy.dev PUBLIC_DATABASE_ANON_KEY=abcd
By default, environment variables can be used only on the server side.
Import them from the $env/static/private module.
1import { 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.
1import { PUBLIC_CLIENT_URL, PUBLIC_DATABASE_ANON_KEY } from '$env/static/public'
If needed, you can add or change the prefixes.
12345678910// ... const config = { kit: { env: { publicPrefix: 'PUBLIC_', // Default value! privatePrefix: 'SECRET_' } } } export default config
1234SECRET_CREDENTIALS=1234 SECRET_API_KEY=5678 PUBLIC_CLIENT_URL=https://heropy.dev PUBLIC_DATABASE_ANON_KEY=abcd
12import { 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.
1234567891011<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!
12345body { 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; }
# Link options
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.
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.
12345export async function load() { return { pageServerData: 'Hello, Server Data!' } }
123456export async function load({ data }) { return { ...data, // pageServerData: 'Hello, Server Data!' pageData: 'Hello, Hydration Data!' } }
123456789<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>
# 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.
1234567import { 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.
12345678910111213141516<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.
12345678910111213import { 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.
12345678910<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.
12345678910export 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.
12345678910111213141516171819202122<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>
Thanks for reading to the end.
Likes and kind comments help keep this blog going!