Prismic Meetup
Prismic's Best Quarter Yet! A Tsunami of ReleasesWatch the recording
Performance & UX
·10 min read

SvelteKit Tutorial: Build a Website From Scratch

In this tutorial, you'll learn how to build and launch a website from scratch with Svelte and SvelteKit.

Why learn SvelteKit?

According to the State of JS 2021 survey, Svelte is the fastest-growing front-end development framework. Svelte is growing with good reason. It is designed for developer ergonomics and web best practices. With Svelte, you’ll make fast, accessible websites.

As the official back-end framework for Svelte, SvelteKit saw immediate uptake by developers when it was first released in beta. SvelteKit 1.0 was released in December of last year and has grown quickly.

Svelte and SvelteKit have many of the same features as other popular web development frameworks, like components, scoped CSS, and file-system based routing. Svelte also includes shortcuts for styling, reactivity, animations, and templating.

What is Svelte?

You might remember Svelte from our Optimized Dev challenge over the summer, but if not (or if you’re new to the challenge 👋), here’s a refresher.

At its core, Svelte is a code compiler. Whereas other frameworks like React and Vue.js generally add code to your web app to make it work in the user's browser, Svelte compiles the code that you write when you build your app. In doing so, it creates very small files and fast websites.

As a compiler, when you write Svelte, it looks a little strange. Here's an example of a .svelte file:

<script>
  let name = 'world';
</script>

<h1> 
  Hello {name}! 
</h1>

That will generate a component that looks like this:

screenshot of a hello world app.

Svelte looks like HTML, with <script> and <style> tags included, but it also adds syntax to make your HTML dynamic — inside curly braces. All of this code gets transformed into vanilla HTML, CSS, and JavaScript with Svelte's compiler.

(If you want more of a refresher on Svelte, head on over to our Svelte tutorial, which takes you through some of its cool features.)

What is SvelteKit?

SvelteKit is a back-end framework for Svelte. While Svelte handles code that runs in the browser — like interactivity and reactivity — SvelteKit gives you infrastructure for the server hosting your app. SvelteKit will provide routing, layouts, static-site generation, API endpoints, and other app features that can only run on a server.

SvelteKit is also great because it has extremely fast hot reloading in development mode. When you click save, changes can appear instantaneously.

An overview of our SvelteKit project

In this project, we’ll build a simple info website with SvelteKit. The website will have a dynamic, component-based layout and dynamic routing. The website content will be managed in a CMS, so that the website manager can add, edit, and remove pages freely. And, we’ll optimize everything for great performance.

In the process of making this website, we’ll learn how to create:

  • The website’s overall style with layouts and global styles
  • Beautiful UI elements using scoped styles, style shorthands, class shorthands, Svelte blocks ({#if}, {#each}, {@const}, and {@html}), and rich text
  • A clean information architecture using active links, file-system based routing, and <title> tags using the <svelte:head> element
  • A great editor experience by using a headless website builder
  • Great page speed scores with responsive images and data from a headless website builder
  • A great developer experience using a component-based website architecture and SvelteKit’s $app module

In the end, we’ll deploy the site so you’ll have your own SvelteKit website live online.

Not signed up for The Optimized Dev?

Staying on the leading edge of web development just got easier. Explore new tech with a fun, scaffolded coding challenge that lands in your inbox once a month.

Setting up our SvelteKit project

I’ve created a boilerplate to help you get started. To install it, run this command in your terminal:

npx prismic-cli@latest theme --theme-url https://github.com/gracemiller23/sveltekit-challenge

Follow the prompts to create your project. This will create a basic boilerplate for you, which includes your Svelte and SvelteKit configuration, a CMS integration, and a few empty components. You’ll be asked to create a “repository name” — you’ll want to make a note of this for a configuration step that will come later.

Once the setup is complete, open the folder you created in your code editor (I use VSCode).

Install dependencies:

npm install

Then run the app in dev mode:

npm run dev

You can now open a basic app in your browser at http://localhost:5173.

A tour of our SvelteKit file structure

All of the relevant files for this project live in the src/ directory. When you get started, you’ll have these files:

src/
├── routes/
│   └── [...path]/
│       └── +page.svelte
└── lib/
    ├── slices/
    │   ├── FunHeading.svelte
    │   ├── ImageBullets.svelte
    │   ├── TextBlock.svelte
    │   ├── TextBox.svelte
    │   └── index.js
    └── prismicio.js

Library

src/lib contains code shared between multiple pages. You can access this directory with the $lib/ alias. There are two things to note here:

  • src/lib/prismicio.js contains some boilerplate for our Prismic CMS connection.
  • src/lib/slices/ contains the components that will render the content for our website, which are called “Slices” in Prismic.

Routing

SvelteKit uses file-system based routing. Each page is defined by a +page.svelte file, and the route for the page is the file’s directory. So src/routes/about/+page.svelte is the page component for the route /about.

What if you want to use one component for more than one route? Then you put the name of the directory in square brackets: src/routes/[path]/+page.svelte. Then, the file will render for any route that matches the pattern. In this case, it would render for /about and /contact, but not /blog/hello-world.

What if you want one component for more than one route segment? For instance, what if you want the same component for /about and also /blog/hello-world? For that, you use Svelte’s rest parameter routing, by prefixing the route name with three periods: src/routes/[...path]/+page.svelte. This component will render for every route on the website.

That’s what we’re doing in this project. All of the pages have the same layout, so we only need one page component to render for every route. src/routes/[...path]/+page.svelte is the page component.

We’ll add to this as we go.

Create a layout

In your project, the file src/routes/[...path]/+page.svelte is the main page component of your app. This component will render a page for any path, so we can use it for the homepage / and any sub-pages, like /contact.

In SvelteKit, we can put global elements — like footers, headers, and global styles, alongside the +page.svelte file in a +layout.svelte file. The layout will wrap the corresponding route and any child routes.

Create the layout file

Create the file src/routes/[...path]/+layout.svelte.

When you create the file, you’ll notice that your page content disappears. That’s because your layout file is empty.

Add one element to your layout:

<!-- src/routes/[...path]/+layout.svelte -->

<slot />

In Svelte, the <slot> element is a placeholder that will render content from a parent component. In a layout file, the <slot> is where your page content will be inserted, so the layout component effectively wraps around your page.

<!-- src/routes/[...path]/+layout.svelte -->
<script>
  import pico from '@picocss/pico';
</script>

<div id="page">
  <nav class="center">
    <a>My Site</a>
  </nav>

  <main class="center">
    <slot />
  </main>

  <footer class="center">© Acme Corporation 2022</footer>
</div>

<style>
  :global(html) {
    overflow-y: scroll;
  }

  #page {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    gap: 5vw;
  }

  .center {
    padding-left: max(1rem, calc(50vw - 350px));
    padding-right: max(1rem, calc(50vw - 350px));
  }

  nav,
  footer {
    text-transform: uppercase;
    font-size: 0.7rem;
    letter-spacing: 0.1px;
  }

  nav {
    background: rgba(0, 0, 0, 0.4);
    font-weight: 500;
    padding-top: 1rem;
    padding-bottom: 1rem;
  }

  footer {
    text-align: center;
    padding: 3rem 0;
    color: #777;
    margin-top: auto;
  }
</style>

What’s going on here?

  • First, we’re importing Pico, and Svelte applies the CSS globally.
  • Then, we have some markup to give our page structure, putting our <slot> element in the middle, where we want the page content to display.
  • Finally, we have some hand-written CSS, including one style rule that uses the :global() CSS pseudo selector, which will apply the rule globally.

We now have a very basic page layout.

Fetch navigation from Prismic

Next, we’re going to fetch data from our CMS — Prismic. A CMS allows you to manage the content of your website — including complicated elements like responsive images, rich text, internal links, and third-party data integrations. Using a CMS means that anyone can edit the website, so you can make changes quickly and safely without opening up your codebase.

First, in scr/lib/prismicio.js update the repoName variable to contain the repository name you created earlier.

Next, we’ll make the <nav> display links managed in Prismic. To do so, create the file src/routes/[...path]/+layout.server.js. This file will run some code server-side to supply data to the layout component. Inside this file, paste in this code:

// src/routes/[...path]/+layout.server.js 

import createClient from '$lib/prismicio';

export async function load({ fetch, request }) {
  const client = createClient({ fetch, request });

  const { data } = await client.getSingle('settings');

  return data;
}

This code imports the client that we configured in src/lib/prismicio.js and then uses it to fetch our settings file from Prismic’s cloud (where your content is stored in what they call a “repository”). It then returns the settings variable for use in the layout component.

In our script file, we’ll add four lines to:

  • Import some tools from @prismicio/helpers
  • Import the page utilities from SvelteKit’s $app module to identify the current page in the navigation
  • Declare a prop with export let data to receive data from +layout.server.js
  • Destructure the navigation from our data prop

Here’s what it will look like:

<!-- src/routes/[...path]/+layout.svelte -->

<script>
  import pico from '@picocss/pico';
  import * as prismicH from '@prismicio/helpers';
  import { page } from '$app/stores';

  export let data;

  const { navigation } = data;
</script>

Next, we can update the navigation element:

Svelte handles rendering logic with a collection of curly-bracket blocks:

  • {#if condition} ... {:else if otherCondition} ... {:else} ... {/if} for conditional rendering
  • {#each array as item} ... {/each} for lists
  • {#await promise} ... {:then value} ... {:catch error} ... {/await} for promises
  • {@html myHTML} for injecting HTML
  • {myJavaScriptVariable} for injecting the result of a JavaScript variable (or expression)

You can see examples for these blocks in the Svelte Society’s cheatsheet.

Here, we’ll use an {#each} block to loop over our navigation items:

<!-- src/routes/[...path]/+layout.svelte -->
	
  <nav class="center">
    {#each navigation as item}
      <a href={prismicH.asLink(item.link)}>{item.link_label}</a>
    {/each}
  </nav>

However, it would also be nice to handle the current page item a little differently. We can add some logic to check if the navigation link is equal to the current page link. If so, we can render it as a <span>:

<!-- src/routes/[...path]/+layout.svelte -->

  <nav class="center">
    {#each navigation as item}
      {@const activeLink = item.link.url === $page.url.pathname}
      {#if activeLink}
        <span>{item.link_label}</span>
      {:else}
        <a href={prismicH.asLink(item.link)}>{item.link_label}</a>
      {/if}
    {/each}
  </nav>

Here, we use Svelte’s {@const} block to declare a local variable: a boolean identifying whether the current item matches the current page. If it does, we just render a <span>. If not, we render an <a>.

Now, we have a navigation menu with links managed in a CMS. Next, we need to render the content for each page.

Fetch page data

Your page component is src/routes/[...path]/+page.svelte. It contains a little boilerplate. Inside the <script> tag we have four imports and an export:

  • We import a <SliceZone> component. This is the component that you will use to render content from Prismic.
  • We import the dev property from SvelteKit’s $app module. This is a boolean that will be true in development and false in production. We pass this as a prop to the <SliceZone> to show errors in development but not production.
  • We import some components from the $lib directory. The src/lib directory is available anywhere in your app as $lib. Inside, we have a slices folder that contains the boilerplate for four Prismic Slices. These Slices will render the content of our app.
  • Finally, we export a variable called data. This is how we declare props in Svelte, and the data prop is how this page component will receive dynamic data.

After the <script> tag, there is some markup in { curly brackets }.

Here, we’re using an {#if} block to conditionally render our <SliceZone> component only if we have a document from the API. In Prismic, each Document represents a page or a settings file.

To proceed, we’ll fetch a document from the Prismic API the same way we fetched our navigation menu. Create src/routes/[...path]/+page.server.js and paste in the following code:

//src/routes/[...path]/+page.server.js

import createClient from '$lib/prismicio';

export async function load({ fetch, request, params }) {
  const uid = params.path.split('/').at(-1) || 'homepage';

  const client = createClient({ fetch, request });

  const document = await client.getByUID('page', uid);

  return { document };
}

This code will parse the URL path and query the API for a document with a matching path. For the homepage path — / — it will query the API for the document with the UID of homepage.

Now, we have data in our page component. Your page should refresh with Slices now rendering. If you’d like, you can delete the conditional logic from your page component in src/routes/[...path]/+page.svelte to simplify it:

<!-- src/routes/[...path]/+page.svelte -->

<script>
  import { SliceZone } from '@prismicio/svelte';
  import { dev } from '$app/environment';

  import * as components from '$lib/slices';

  export let data;
</script>

<SliceZone slices={data?.document?.data?.body} {components} {dev} />

Add a page title

Every web page should have a <title> tag in the header. To inject that tag, you can use Svelte’s <svelte:head> element. Import your tools from @prismicio/helpers in order to render your title element, then add a <svelte:head> element containing a <title> tag to your page:

<!-- src/routes/[...path]/+page.svelte -->

<script>
	import { SliceZone } from '@prismicio/svelte';
        //import Prismic's helpers package
	import * as prismicH from '@prismicio/helpers';
	import { dev } from '$app/environment';

	import * as components from '$lib/slices';

	export let data;
</script>
//inject your title tag
<svelte:head>
	<title>{prismicH.asText(data?.document?.data?.title)}</title>
</svelte:head>

<SliceZone slices={data?.document?.data?.body} {components} {dev} />

Inside the <title> tag, we’re rendering the document’s title property as plain text using prismicH.asText().

Create Slices

The project boilerplate includes four Slices:

  • src/lib/slices/FunHeading.svelte
  • src/lib/slices/ImageBullets.svelte
  • src/lib/slices/TextBlock.svelte
  • src/lib/slices/TextBox.svelte

Instead of coding a single page component, we’ll code each of these Slices individually. Then you can use Prismic’s custom website builder interface to add and rearrange these Slices to build pages using the components. (You can see the interface by visiting your dashboard and checking out the repository you created earlier.)

Code the TextBlock Slice

To start with, open src/lib/TextBlock.svelte. This Slice is going to display formatted text. Delete the contents of the file, and then paste in this code:

<!-- src/lib/TextBlock.svelte -->

<script>
  import * as prismicH from '@prismicio/helpers';
	
  export let slice;
</script>

{@html prismicH.asHTML(slice.primary.text)}

Here, we’re importing a Prismic utility to handle Rich Text. We use the utility — asHTML() — to generate a string of formatted HTML using content from the Prismic API and then we use Svelte’s {@html} block to inject the string into our app.

If you refresh your homepage, you should see that one of your Slices is now rendering content with formatting.

Code the TextBox Slice

Next, we’ll code the TextBox Slice. This is going to be a callout box with an emoji icon in the corner. For starters, in src/lib/TextBox.svelte create an <article> element. Inside, create two <divs>: one with the class of "emoji", to render our emoji; and one with the class of "text", to render our text just like we did in the TextBlock Slice:

<!-- src/lib/TextBox.svelte -->

<script>
  import * as prismicH from '@prismicio/helpers';

  export let slice;
</script>

<article>
  <div class="emoji">{slice.primary.emoji}</div>
  <div class="text">
    {@html prismicH.asHTML(slice.primary.text)}
  </div>
</article>

<style>
  article {
    display: flex;
    align-items: flex-start;
    gap: 1.2rem;
    padding: 2rem 2rem 2rem 1.4rem;
  }

  .emoji {
    font-size: 2rem;
  }
</style>

Finally, we’ll make the layout more customizable by giving editors the ability to choose whether the emoji goes on the left or right side of the box.

Svelte includes a helpful class shorthand to facilitate this. By writing class:variable (using a colon) we can apply a class with the name of variable conditionally only if variable is true:

<!-- src/lib/TextBox.svelte -->

<script>
  import * as prismicH from '@prismicio/helpers';

  export let slice;

  // Check if the emoji should be at right or left
  const reverse = slice.slice_label === 'emoji_right';
</script>

<!-- Conditionally apply a class -->
<article class:reverse>
  <div class="emoji">{slice.primary.emoji}</div>
  <div class="text">
    {@html prismicH.asHTML(slice.primary.text)}
  </div>
</article>

<style>
  article {
    display: flex;
    align-items: flex-start;
    gap: 1.2rem;
    padding: 2rem 2rem 2rem 1.4rem;
  }

  .emoji {
    font-size: 2rem;
  }

  /* If the class is applied put the emoji at the right */
  .reverse {
    flex-direction: row-reverse;
    padding: 2rem 1.4rem 2rem 2rem;
  }
</style>

Code the ImageBullets Slice

Our third Slice is an image with some bullets next to it.

To serve optimized images responsively, we’ll use Prismic’s asImageWidthSrcSet() helper, which will return an object containing a src string and a srcset string.

Paste in this code to create the basic template for your Slice in src/lib/slices/ImageBullets.svelte:

<!-- src/lib/slices/ImageBullets.svelte -->

<script>
  import * as prismicH from '@prismicio/helpers';

  export let slice;

  const { src, srcset } = prismicH.asImageWidthSrcSet(slice.primary.image);
</script>

<div class="layout box">
  <img {src} {srcset} alt={slice.primary.image.alt} />
  <ul>
    <li>{prismicH.asHTML(slice.items[0].bullet, null, { paragraph: ({ children }) => children })}</li>
  </ul>
</div>

<style>
  img {
    width: 40%;
    border-radius: 1rem;
    aspect-ratio: 0.9;
    object-fit: cover;
    flex: 1.2;
  }

  .box {
    display: flex;
    justify-content: center;
    align-items: center;
    flex: 1;
    margin-top: 5vw;
    margin-bottom: 5vw;
  }

  ul {
    margin: 0;
    padding-left: 2rem;
    flex: 1;
  }

  .box li {
    font-weight: 500;
    color: black;
    padding-left: 0.5rem;
    margin-bottom: 1rem;
  }

  .box li:last-child {
    margin-bottom: 0;
  }
</style>

Note: The third parameter to the asHTML() helper is an HTML Serializer. In this case, it is just removing the <p> tags from paragraph elements to simplify our formatting. You can ignore it for the purpose of this tutorial.

Render a list

The bullet list in this Slice is only rendering the first element. Let’s update the code so that the entire list will render. To do this, we can use Svelte’s each block.

<!-- src/lib/slices/ImageBullets.svelte -->

<div class="layout box">
	<img {src} {srcset} alt={slice.primary.image.alt} />
	<ul>
		{#each slice.items as item}
			<li>{prismicH.asHTML(item.bullet, null, { paragraph: ({ children }) => children })}</li>
		{/each}
	</ul>
</div>

Keep challenging yourself

Add some flair

Each list item includes text and an emoji. Instead of using standard list bullets, punctuate each list item with an emoji.

Learn a Svelte shorthand

There are many ways to create an emoji-bullet list. In Svelte, one way you can do it is with the style shorthand. See if you can use Svelte’s style shorthand to render your emoji list.

Hint 1: Google “list-style-type” for insight into how to customize a list.

Hint 2: &quot; marks can be tricky here!

Code the FunHeading Slice

This is our most basic Slice. It’s just a text string that we’re going to use as the title.

<!-- src/lib/slices/FunHeading.svelte -->

<script>
  import * as prismicH from '@prismicio/helpers';

  export let slice

</script>

<h2>{slice.primary.text}</h2>

Now have fun!

This one is up to you. Use what you’ve learned about Svelte to create a fun title Slice. If you want an extra challenge, you could incorporate some kind of reactivity. Some examples:

  • Change the color of the header when a user clicks on it
  • Let the user select the font of the header
  • Include time-sensitive text under the title, like “Good morning / good evening”

Deploy your SvelteKit site

Make sure your project is pushed to GitHub. You'll need to initialize your project as a git repository:

git init

Add all of your files to staging:

git add .

And commit them:

git commit -m "Init"

In GitHub, create a new repository and follow the instructions to push your project to GitHub.

Visit Netlify or Vercel and create an account or log in. Click New project or New site. Follow the instructions to deploy your new GitHub repo. You can leave the deploy settings as is.

Congratulations!

Once you deploy your site, it should be live! Congratulations on building a website with SvelteKit 🎉

To recap, here’s what we went over:

  • You initialized a boilerplate from a Prismic theme
  • You added a layout with markup, styles, and active links
  • You integrated a CMS to safely and conveniently separate code from content
  • You built an entire website with a component-based architecture
  • You used SvelteKit’s advanced routing features to generate many pages from a single component
  • You learned to use a bunch of Svelte features, including templating blocks, modules, and class and style shorthands

Share your project

Now that your project is online, we’d love to see it. Tag us on Twitter — @prismicio and @samlfair — and share your project on the Prismic Community Forum.

Resources

To dig deeper into SvelteKit, have a look at the official documentation for building a website with Prismic and Svelte and these handy resources:

FAQs about Svelte and SvelteKit

What is Svelte?

At its core, Svelte is a code compiler. Whereas other frameworks like React and Vue.js generally add code to your web app to make it work in the user's browser, Svelte compiles the code that you write when you build your app. In doing so, it creates very small files and fast websites.

As a compiler, when you write Svelte code, it looks a little strange. Svelte looks like HTML, with <script> and <style> tags included, but it also adds syntax to make your HTML dynamic — inside curly braces. All of this code gets transformed into vanilla HTML, CSS, and JavaScript with Svelte's compiler.

Article written by

Sam Littlefair

Sam is a Canadian in France preoccupied with journalism, web publishing, JavaScript, meditation, and travel. He works on documentation at Prismic.

More posts
Sam Littlefair

Join the discussion