How to setup a simple static website using Svelte (with login)

jlewis92

Jack Lewis

Posted on May 19, 2023

How to setup a simple static website using Svelte (with login)

Preface

Svelte is an up and coming JavaScript framework being developed by Rich Harris (and the Svelte team). It's gained acclaim for being very simple to use, reducing boilerplate and generally being a nice platform to develop new web apps on. This article is an attempt to explain how you can leverage the power of Svelte to create modern static sites that can be hosted from simple file storage like S3 in AWS.

While hosting the application is outside the scope of this article, I do have another here which explains how to do the hosting within it.

It needs to be understood, that converting Svelte to run in a static context will; mean that a lot of the features touted by the Svelte team will be disabled. However, I still feel it's worth it, as writing in Svelte is still a really nice experience.

Benefits

The main benefit of this approach is cheap hosting. You don't need to run a docker container or a machine in your cloud service provider of choice, which reduces the cost of the website as you're only paying for serving files. Additionally, many cloud service providers have products that allow you to leverage a content delivery network easily. Finally, static applications are fast because there's no need communicate backwards and forwards with a server, it just downloads some files to the browser.

Drawbacks

Essentially, all server based code in a Svelte will not work using static hosting. Given Svelte is split into code that runs everywhere and code that runs in the server, this can be a little annoying. You'll need to remove all .ts/.js files that contain the server keyword (i.e.: +page.server.ts or +server.js) as these files won't work. Additionally, some features ofSvelte won't work such as dynamic routing and server side rendering.

Tutorial

Prerequisites

You'll need an up-to-date version of your favourite node based package manager. I'm using npm, but pnpm and yarn both work.

Installation

to start with we need to create aSvelte app from the terminal:



npm createSvelte@latest


Enter fullscreen mode Exit fullscreen mode

It will ask you a few questions about how you want to set up your project. For the cases of this demo, I went with a skeleton project and with TypeScript. You select options (such as prettier) using the space bar.

You then need to run the installation:



npm install


Enter fullscreen mode Exit fullscreen mode

Just for context, you can run npm build at this stage to see what the built output looks like without convertingSvelte to run as static. You can see it just generates a large number of files in the .svelte-kit folder. You can run from this directory using npm run preview:

static example

Converting to static

It's really quite straight forward to convert aSvelte app to run in a static context. Svelte does this using adapters. By default, Svelte uses the auto adapter which works best for general cases. There are a few different adapters, and you choose which one based on what you're doing. For example, there are adapters based on providers (such as Netlify or Vercel) however, the one we're interested in, is adapter-static which makes Svelte run like a static site generator.

In order to get this to work, all you need to do is to install the adapter-static package:



npm i -D @sveltejs/adapter-static


Enter fullscreen mode Exit fullscreen mode

Then go to svelte.config.js at the top level, and change the line talking about adapter-auto to say adapter-static. I.e.:



import adapter from '@sveltejs/adapter-auto';


Enter fullscreen mode Exit fullscreen mode

to



import adapter from '@sveltejs/adapter-static';


Enter fullscreen mode Exit fullscreen mode

NOTE: there's a lot of additional settings you can set for this, but for the purposes of this guide, we're not going change any of the defaults.

At this point you can try and build the application, and it will instantly spit out a warning about dynamic routes in the src/routes folder. It gives you a bunch of options on how to fix this, but the simplest is to create a file called layout.{ts|js} in src/routes and then place the following into it:



export const prerender = true;


Enter fullscreen mode Exit fullscreen mode

This forces all files in the src/routes folder to be prerendered, which is what we need for a static site.

At this point, if you run npm run build you should see a build folder created that contains an index.html, which is the entry point for the static site:

static generated

UI library

Usually, one of the first things I do on creating a new web app is to throw a UI library in to help style components. There are several UI libraries that can be used by Svelte, but in this case I went with daisyUI because it's a fairly popular UI library which includes tailwind. To install daisyUI, you first need to install tailwind. There's a few different ways to do this (such as this guide), but the easiest way I've found is the following command, which also adds PostCSS and AutoPrefixer:



npxSvelte-add@latest tailwindcss


Enter fullscreen mode Exit fullscreen mode

You should then be able to install daisyUI:



npm install daisyui


Enter fullscreen mode Exit fullscreen mode

The final step for daisyUI is to then add a plugin for daisyUI into the tailwind.config.cjs folder. It should look something like this:



/** @type {import('tailwindcss').Config}*/
const config = {
    content: ['./src/**/*.{html,js,svelte,ts}'],

    theme: {
        extend: {}
    },

    plugins: [require("daisyui")],
};

module.exports = config;


Enter fullscreen mode Exit fullscreen mode

You can tell that daisyUI is installed correctly in 2 ways, the first is to add a styled component, like a button:



<button class="btn">Button</button>


Enter fullscreen mode Exit fullscreen mode

styled button

or alternatively, you can run npm run build or npm run dev and the output tells you that daisyUI is installed:

daisy UI

Animations

While Svelte has a pretty good built-in library of animations, if I do need some more custom animations, I don't actually use a library, but I do use Animista to pick out something which works for me.

Repository Setup

I talk about this in more detail in my article here, but I'm a big fan of adding pre-commit into my git repositories to help out.

Login

Whether it's a good idea or not, it's definitely possible to add login functionality to a static site. This can be done through JSON web tokens and the OAuth Authorization code flow. How this works is that we offload authorization to a third party, that then returns a token that verifies a user is logged. That token can then be used to authenticate with an API somewhere. This can be a little difficult to get your head around, but this essentially what happens when you click the "log in with Google/Facebook/GitHub" button on a website and is known as "Single Sign On" or "SSO".

We're specifically using the authorization code flow as this is the one needed for when the entire website is served to the browser i.e. when using a static website.

If you do want to add login functionality to a static website, you need to make sure that all data related to a user comes in from an authenticated backing API. For example, API gateway in AWS. The reason for this is that any data you serve to a website can be modified by the client, meaning that somebody could "force" a login by just changing the JavaScript locally. You need to stop this by making something like an OAuth token required on every call to the backend, as this is verifying that a user has access to those resources.

Why?

This can be a little counterintuitive to do as if you're doing this, why don't you just use server side features of Svelte and avoid a separate API all together, which would likely be faster than offloading the calls to the browser. The answer to this is mainly in cases where you have a public API being used by others, as well as yourself or that you're using the same resources with multiple OAuth flows, such as the client credential flow for machine to machine calls. As such, this can be useful on admin portals for application support.

How to add

Just about every cloud provider has their own implementation of a JavaScript library that can offload the complexity of writing your own implementation. For example, AWS has Amplify, GCP has Firebase and Azure has Identity. You can also use a more generic library like Auth.js. However, I'm going to go with Auth0 in this guide as I've not used this service before, and it's a slightly more "neutral" option than going for a service offered directly by a cloud provider. Additionally, Auth0 will allow you to get started without needing to add a credit card, which makes it useful for demonstrations.

Picking the best service for your needs is outside the scope of this article, but safe to say a lot of considerations to keep in mind. This can be everything from a cloud provider offering a tighter integration with their own products to the number of monthly free users you're allowed (called MAU's) to the number of SSO providers offered for integration.

Setup

To start with you need to sign up for an account with Auth0 and add the Auth0 single page app SDK to your Svelte application:



npm install @auth0/auth0-spa-js


Enter fullscreen mode Exit fullscreen mode

After this, you need to go into the front page of Auth0, and click "create application" and select "Single Page Application":

Auth0 spa

There's a large number settings you can use, but that's outside this guide. However, a nice feature of the documentation is that if you're logged in, the documentation will be updated to use details for your account:

Auth0 documentation

You then need to set callback and logout URL's. Svelte usually sets the startup port as 5173, but pick whatever you need:

Auth0 callback

I've put localhost in at the moment as I'm just running this locally. Obviously if you're actually running this, just set the callback and logout URL's to the valid paths there.

At this point all you need to do to set up the login functionality is to construct the auth0 object:



import { Auth0Client } from '@auth0/auth0-spa-js';

const auth0 = new Auth0Client({
    domain: '<domain>',
    clientId: '<client id>'
});


Enter fullscreen mode Exit fullscreen mode

and then you can start the login process by setting up a button which calls Auth0:



async function login() {
    await auth0.loginWithRedirect({
        authorizationParams: {
            redirect_uri: 'http://localhost:5173/'
            }
    })
}

<button on:click={login} class="btn">Login</button>


Enter fullscreen mode Exit fullscreen mode

If you press the button, it should take you to the Auth0 login page where you can now sign up a user. It also handles email verification and SSO. Once you do this, it should redirect you back to your application. You can tell it's working at this point if you can see that you have token in the URL as a query parameter. For example:



http://localhost:5173/?code=tlcjskafaUOODPicz_P83g6YrwgiFxOO7PXv0gNZAKDsC&state=ekdCQzFRZjllTmJyOTUwcU5XWjNqV0RQN04wZC11alZ0UHB1Q0hPNElYTQ%3D%3D


Enter fullscreen mode Exit fullscreen mode

In order to handle this, we need to call the auth0.handleRedirectCallback() function on page load. This can be done using onMount in Auth0:



import { onMount } from 'svelte';

onMount(async () => {
    await auth0.handleRedirectCallback()
    .then(async () => {
        const user = await auth0.getUser();
        console.log(user);
    })
    .catch(() => {
        console.log("not authenticated");
    });
})


Enter fullscreen mode Exit fullscreen mode

Logout functionality is just as straight forward:



async function logout() {
    await auth0.logout();
}


Enter fullscreen mode Exit fullscreen mode

For context, here's a full minimal implementation of this:



<script lang="ts">
    import { Auth0Client, User } from '@auth0/auth0-spa-js';
    import { onMount } from 'svelte';

    const auth0 = new Auth0Client({
        domain: '<auth0 domain>',
        clientId: '<auth0 client id>'
    });

    let user: User | undefined;

    async function login() {
        await auth0.loginWithRedirect({
            authorizationParams: {
            redirect_uri: 'http://localhost:5173/'
            }
        })
    }

    async function logout() {
        await auth0.logout();
    }

    onMount(async () => {
        await auth0.handleRedirectCallback()
        .then(async () => {
            user = await auth0.getUser();
        })
        .catch(() => {
            console.log("not authenticated");
        });
    })

</script>

<button on:click={login} class="btn">Login</button>
<button on:click={logout} class="btn">Logout</button>

{#if user != undefined}
    <p>hello {user.nickname}</p>
{/if}


Enter fullscreen mode Exit fullscreen mode

The final thing to talk about, is that if you want to get the token to use in an authorization header for retrieving data through an API, you can get this using either auth0.getTokenSilently() or auth0.getTokenWithPopup() depending on your needs.

While it's outside the scope of this article, Auth0 provides documentation about how to integrate with all the major cloud providers. For example, the documentation for how to implement Auth0 with AWS API Gateway is found here.

💖 💪 🙅 🚩
jlewis92
Jack Lewis

Posted on May 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related