Skip to main content eteppo

Understanding Svelte/SvelteKit By Reading The Source Of This Website

Published: 2023-08-31
Updated: 2023-08-31

The source of the website you’re on now is structured like this except for the hidden items (calling tree --filelimit on a terminal):

.
├── jsconfig.json
├── LICENSE
├── node_modules  [174 entries exceeds filelimit, not opening dir]
├── package.json
├── package-lock.json
├── src
│   ├── app.html
│   ├── global.d.ts
│   ├── lib
│   │   ├── assets
│   │   │   ├── js
│   │   │   │   ├── fetchPosts.js
│   │   │   │   └── store.js
│   │   │   └── scss
│   │   │       ├── _animation.scss
│   │   │       ├── _code.scss
│   │   │       ├── _components.scss
│   │   │       ├── _forms.scss
│   │   │       ├── global.scss
│   │   │       ├── _header-and-footer.scss
│   │   │       ├── _layout.scss
│   │   │       ├── _prism.scss
│   │   │       ├── _typography.scss
│   │   │       ├── _utilities.scss
│   │   │       └── _vars.scss
│   │   ├── components
│   │   │   ├── Callout.svelte
│   │   │   ├── Footer.svelte
│   │   │   ├── HamburgerMenuButton.svelte
│   │   │   ├── Header.svelte
│   │   │   ├── MainNav.svelte
│   │   │   ├── NavItem.svelte
│   │   │   ├── Pagination.svelte
│   │   │   ├── PostsList.svelte
│   │   │   └── svg
│   │   │       ├── HamburgerSVG.svelte
│   │   │       └── XSVG.svelte
│   │   ├── config.js
│   │   └── posts  [32 entries exceeds filelimit, not opening dir]
│   └── routes
│       ├── api
│       │   ├── posts
│       │   │   ├── count
│       │   │   │   └── +server.js
│       │   │   └── page
│       │   │       └── [page]
│       │   │           └── +server.js
│       │   ├── posts.json
│       │   │   └── +server.js
│       │   └── rss.xml
│       │       └── +server.js
│       ├── blog
│       │   ├── category
│       │   │   ├── [category]
│       │   │   │   ├── page
│       │   │   │   │   ├── [page]
│       │   │   │   │   │   ├── +page.server.js
│       │   │   │   │   │   └── +page.svelte
│       │   │   │   │   ├── +page.server.js
│       │   │   │   │   └── +page.svelte
│       │   │   │   ├── +page.server.js
│       │   │   │   └── +page.svelte
│       │   │   ├── page
│       │   │   │   └── [page]
│       │   │   │       ├── +page.server.js
│       │   │   │       └── +page.svelte
│       │   │   ├── +page.server.js
│       │   │   └── +page.svelte
│       │   ├── page
│       │   │   ├── [page]
│       │   │   │   ├── +page.server.js
│       │   │   │   └── +page.svelte
│       │   │   ├── +page.server.js
│       │   │   └── +page.svelte
│       │   ├── +page.server.js
│       │   ├── +page.svelte
│       │   └── [post]
│       │       ├── +page.js
│       │       └── +page.svelte
│       ├── +error.svelte
│       ├── +layout.js
│       ├── +layout.svelte
│       └── +page.md
├── static
│   ├── favicon.png
│   ├── images  [32 entries exceeds filelimit, not opening dir]
│   └── link.svg
├── svelte.onfig.js
└── vite.config.js

Note the open license.

MIT License

Copyright (c) 2021 Josh Collinsworth

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

The app.html only contains global <head> markups used in all pages and a body with a container <div id="svelte">%sveltekit.body%</div>. Stuff will be injected to the % spot except if they have been marked with <svelte:head>.

src/routes/ defines routing in the app just like a file system. Everything with server.js will be run on the web server instead of the web client (browser). In Svelte, you describe your app as a hierarchy of components. The top of the hierarchy in this case is layout.svelte. Each page has a header, a place for content, and a footer.

<!-- Style depends on whether menu is open which is tracked in a state variable (store). -->
<div class="layout" class:open={$isMenuOpen}>
	<Header />
	<!-- When key changes (path), the contents (page) are recreated. -->
	{#key data.path}
		<main
			id="main"
			tabindex="-1"
			<!-- Directives to attach a transition function to the content -->
			in:fade={transitionIn}
			out:fade={transitionOut}
		>
			<slot />
		</main>
	{/key}
	<Footer />
</div>

We have two components (Header, Footer), two state variables (isMenuOpen, data), two normal/constant variables (transitionIn, transitionOut), and one function (fade). They are imported in the <script> part of layout.svelte.

<script>
	// Styles have been defined globally but normally you'd do it in this component file within <style>.
        import '$lib/assets/scss/global.scss'

	// Components.
        import Header from '$lib/components/Header.svelte'
        import Footer from '$lib/components/Footer.svelte'

	/ State-tracking variables.
        import { currentPage, isMenuOpen } from '$lib/assets/js/store'

	// Functions
        import { fade } from 'svelte/transition'

	// New state variable defined in this component. It's value is set below.
        export let data

	// Constants for this component.
        const transitionIn = { delay: 150, duration: 150 }
        const transitionOut = { duration: 100 }

	// $-labelled function calls react to changes in the state.
	// When currentPage changes, it sets the new value to data.path.
        $: currentPage.set(data.path)
  
</script>

The top level +page.md is the home page content (markdown) and will be preprocessed and then injected to the slot, except if there’s an error, +error.svelte is used (just HTML). If you navigate to blog, you will see a +page.svelte like this:

<script>
	// Import child components (list of posts and pagination).
	import PostsList from '$lib/components/PostsList.svelte'
	import Pagination from '$lib/components/Pagination.svelte'

	// Normal variable holding a string.
	import { siteDescription } from '$lib/config'

	// Local state variable.
	export let data
</script>

<svelte:head>
	<title>Blog</title>
	<meta data-key="description" name="description" content={siteDescription}>
</svelte:head>

<h1>Blog</h1>

<!-- data actually holds data.posts and data.total. Loading happens on the server-side. -->
<PostsList posts={data.posts} />

<Pagination currentPage={1} totalPosts={data.total} />

data.posts and data.total are loaded in the server-file.

export const load = async ({ url, fetch }) => {
	const postRes = await fetch(`${url.origin}/api/posts.json`)
	const posts = await postRes.json()

	const totalRes = await fetch(`${url.origin}/api/posts/count`)
	const total = await totalRes.json()

	return { posts, total }
}

When you navigate to a post, you will see this:

<script>
// Local state variable again.
export let data

// It holds content and metadata.
const {
	title,
	excerpt,
	date,
	updated,
	coverImage,
	coverWidth,
	coverHeight,
	categories 
} = data.meta

const { PostContent } = data
</script>

<svelte:head>
	<title>{title}</title>
	<meta data-key="description" name="description" content="{excerpt}">
	<meta property="og:type" content="article" />
	<meta property="og:title" content={title} />
	<meta name="twitter:title" content={title} />
	<meta property="og:description" content={excerpt} />
	<meta name="twitter:description" content={excerpt} />
	<meta property="og:image:width" content={coverWidth} />
	<meta property="og:image:height" content={coverHeight} />
</svelte:head>

<!-- Post is image, title, dates, content, and categories. -->
<article class="post">
	<img
		class="cover-image"
		src="{coverImage}"
		alt=""
		style="aspect-ratio: {coverWidth} / {coverHeight};"
		width={coverWidth}
		height={coverHeight}
	/>

	<h1>{ title }</h1>
	
	<div class="meta">
		<b>Published:</b> {date}
		<br>
		<b>Updated:</b> {updated}
	</div>

	<!-- PostContent is a component constructor. -->
	<svelte:component this={PostContent} />

	<!-- If not null -->
	{#if categories}
		<aside class="post-footer">
			<h2>Posted in: </h2>
			<ul>
				<!-- A list of links -->
				{#each categories as category}
					<li>
						<a href="/blog/category/{category}/">
							{ category }
						</a>
					</li>
				{/each}
			</ul>
		</aside>
	{/if}
</article> 

Again, for loading the data stuff, we have the load function in the corresponding +page.js:

import { error } from '@sveltejs/kit'

export const load = async ({ params }) => {
	try {	
		const post = await import(`../../../lib/posts/${params.post}.md`)

		return {
			PostContent: post.default,
			meta: { ...post.metadata, slug: params.post } 
		}
	} catch(err) {
		throw error(404, err)
	}
}

Now, let’s look at the Header component.

<script>
	// Two child components.
	import MainNav from './MainNav.svelte'
	import HamburgerMenuButton from './HamburgerMenuButton.svelte'

	// Regular variable.
	import { siteTitle } from '$lib/config'

	// Regular function.
	const focusMain = () => {
		const main = document.querySelector('main');
		main.focus();
	}
</script>

<header>
	<!-- You can navigate to "blabla#main" to call focusMain. -->
	<a on:click|preventDefault={focusMain} class="skip-to-content-link" href="#main">
		Skip to main content
	</a>
	
	<a href="/" class="site-title">
		{siteTitle}
	</a>
	
	<HamburgerMenuButton />
	<MainNav />

</header>

It’s components like this all the way down. And thus we can stop our tour here for now.