Build a Single Page Svelte Application with Svelte Router

📅 2 years ago 🕒 13 min read 🙎‍♂️ by Madza

Build a single-page application in Svelte with svelte-spa-router

The two main design patterns for web apps today are multi-page applications (MPA) and single-page applications (SPA). Each comes with significant differences in handling user requests.

MPA reloads the whole page every time there is a request for new data. In SPA the page reload never happens as all the static files are loaded on the initial load and only the fetched data is updated in the view when necessary.

SPAs are usually faster than multi-page approaches and improve the UX significantly. However, their dynamic behavior also comes with a drawback. Since the state of the app is not assigned to the URL, it can be a challenge to retrieve the view on the next load.

In this article, we will create a single-page application in Svelte and further focus on implementing a routing mechanism with svelte-spa-router. It is developed and maintained by the Alessandro Segala and some other contributors.

We will build a blog application that will include direct routes, routes with parameters, and wildcards to handle the rest of the routes. For reference here is the source code of the final project:

Demo

Why svelte-spa-router?

The svelte-spa-router paths are hash-based. This means the application views are stored in the fragment of the URL starting with the hash symbol (#).

For example, if the single page application lives in the App.svelte file the URL https://mywebsite.com/#/profile might be used to access the user profile.

The fragment starting with the hash (#/profile) is never sent to the server meaning the user is not required to have a server on the backend to process the request. Traditional routes like /profile would always require a server.

svelte-spa-router is very easy to use, has great support for all the modern browsers, and, thanks to its hash-based routing, is well optimized for the use of single-page applications.

Setting up the app

We will use the official template of Svelte to scaffold a sample application via degit. Open your terminal and run the following command: npx degit sveltejs/template svelte-spa-router-app.

Then change your current working directory to the newly created folder by running cd svelte-spa-router-app and install all the necessary packages by running npm install.

Once the packages have been installed start a development server by running npm run dev.

By default the Svelte apps run on port 5000, so navigate to localhost:5000 in your browser and you should be able to see the newly created app.

Svelte default app

We will use svelte-spa-router package (60.9 kB unpacked) as the basis for the router. Install it by running the following command:

npm install svelte-spa-router

We will also need a couple of small npm helper packages like url-slug to create URLs for the applications and timeago.js that will help to calculate the time since the publishing of the articles. You can install both by running a single command:

npm install url-slug timeago.js

Adding sample data

For the simplicity of this tutorial, we will simulate the blog data that would normally come from a database by storing it into a variable blogs.

Navigate to the project root directory, create a new file data.js, and include the following code:

export const blogs = [
  {
    title: "17 Awesome Places to Visit in Germany",
    content:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    image: "https://picsum.photos/id/1040/800/400",
    publishDate: "2021/12/12"
  },
  {
    title: "21 Essential Backpack Items for Hiking",
    content:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    image: "https://picsum.photos/id/1018/800/400",
    publishDate: "2021/11/17"
  },
  {
    title: "10 Safety Tips Every Traveller Should Know",
    content:
      "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    image: "https://picsum.photos/id/206/800/400",
    publishDate: "2021/09/06"
  }
];

Notice that we used export in front of the array constant. This way we will be able to import the array in any file of the app and use its data when necessary.

Creating components

Next, make a new folder components in the project's root and add separate files Card.svelte, Home.svelte, Article.svelte and NotFound.svelte inside it.

Open the file Card.svelte and include the following code:

<script>
import { link } from "svelte-spa-router";
import urlSlug from "url-slug";
export let title, description, image, publishDate;
</script>

<div class="wrapper">
  <a href={image} target="_blank">
    <img src={image} alt="img" >
  </a>
	<div>
		<h2 class="title"><a href={`/article/${urlSlug(title)}`} use:link>{title}</a></h2>
		<p class="description">{description.substring(0, 180)}...</p>
		<p>Published: {publishDate}</p>
	</div>
</div>

<style>
  .wrapper {
    display: grid;
    grid-template-columns: repeat(2, auto);
    gap: 20px;
    padding: 20px 0;
  }

  .title,
  .description {
    margin: 0 0 10px 0;
  }

  img {
    border-radius: 5px;
    max-width: 230px;
    cursor: pointer;
  }

  @media only screen and (max-width: 600px) {
    .wrapper {
      grid-template-columns: 1fr;
    }

    img {
      max-width: 100%;
    }
  }
</style>

The Card component will display the articles in the landing area. We first imported necessary helpers and then exported the props title, description, image, and publishDate to pass in once using the component inside the app.

Then we created a two-column layout for the card, where the cover image is shown on the left and the title, description, and the publishDate are shown on the right. We added padding to the card and a gap between the two columns.

We set the cursor to pointer when hovered over the image and made it open in the new tab once clicked. We also made the layout switch to 1 column layout and the image takes all of the available width of the parent when the width of the viewport is 600px or less.

Next, open the file Home.svelte and include the following code:

<script>
import urlSlug from "url-slug";
import { format } from "timeago.js";
import Card from "./Card.svelte";
import { blogs } from "../data.js";
</script>

<h1>All your traveling tips in one place</h1>
{#each blogs as blog, i}
	<Card title={blog.title} description={blog.content} image={blog.image} publishDate={format(blog.publishDate)}/>
{/each}

We first imported urlSlug helper to create URL slugs from article titles, format to measure the time that has passed since posting, Card component we just created, and blogs data array. Then we looped through each post by providing necessary props for Card.

Then, open the file Article.svelte and include the following code:

<script>
	import urlSlug from "url-slug";
	import { format } from "timeago.js";
	import { blogs } from "../data.js";
	import NotFound from "../components/NotFound.svelte";

	export let params = {};
	let article;

	blogs.forEach((blog, index) => {
	  if (params.title === urlSlug(blog.title)) {
	    article = blog;
	  }
	});
</script>

{#if article}
	<div>
		<h1>{article.title}</h1>
		<p>Published: {format(article.publishDate)}</p>
		<img src={article.image} alt="img">
		<p>{article.content}</p>
	</div>
{:else}
	<NotFound/>
{/if}

<style>
	img {
	  max-width: 100%;
	}

	p {
	  text-align: justify;
	}
</style>

Again, we first imported both helpers to work with slugs and dates, imported the blogs array for the data, and also imported NotFound component we will create in the next step to use if the article is not available.

In the script tags we looped through each article in the blogs array and checked if the title of the article equals the current :title parameter in the URL (for example, if the title of the article is "My article title 1", then the parameter in the URL should be "my-article-title-1").

If the :title parameter matches the title, that means the article is available and we render it. If it is not available we render the NotFound component instead.

We also set the cover image of the Article to fill all of the available width of the parent and made the sides of the text to be justified.

Finally, open the file NotFound.svelte and include the following code:

<script>
import { link } from "svelte-spa-router";
</script>

<h1>We are sorry!</h1>
<p>The travel tips you are looking for do not exist.</p>
<img src="https://picsum.photos/id/685/800/400" alt="img">
<p>We still have other travel tips you might be interested in!</p>
<a href="/" use:link>
	<h2>Take me home →</h2>
</a>

<style>
	img {
	  width: 100%;
	}
</style>

The NotFound component will be used for all the routes that are not defined. For example, if someone tries to visit article/aa-bb-cc-dd the user will see the NotFound view.

We imported the link from svelte-spa-router, so we can later use it in the use:link action. Then we rendered a text message to inform the user that the route is not available and included an image to make the error screen visually more appealing.

Creating the routing file

In svelte-spa-router the routes are defined as an object, consisting of the keys for the routes and values for the components. We will purposely build a router to cover all of the use cases: direct routes, routes including parameters, and wildcards to catch the rest of the routes.

The syntax of the direct route is /path. For the simplicity of this tutorial, we will use just one direct route / to take users home, but you can include as many as you want like /about, about-us, /contact, and many more based on your needs.

Next, you might want to include some specific parameters in your view to fetch the data. The syntax for this is /path/:parameter. In our app, we will use the parameters to load the right content for the article view by /article/:title. If you want you can even chain multiple parameters like /article/:date/:author.

Finally, the user can use wildcards to control all the routes. We will use a wildcard * to catch all the non-existent routes, displaying a NotFound view for the user. Furthermore, you can also include wildcards for the path of defined routes, for example /article/*.

Now let's create a separate routes.js file in the project root, import the components and assign them to the routes:

import Home from "./components/Home.svelte";
import Article from "./components/Article.svelte";
import NotFound from "./components/NotFound.svelte";

export const routes = {
  "/": Home,
  "/article/:title": Article,
  "*": NotFound
};

It is important to keep in mind that the Router will work on the first matched route in the object, so the order in the routes object matters. Make sure that you always include wildcard as last.

Using router in the app

If you managed to complete all the previous steps of setting up the app, modeling the data, and creating components, the last phase of using the router in an app will be very straightforward.

Open the App.svelte file in the src folder and include the following code:

<script>
  import Router, { link } from "svelte-spa-router";
  import { routes } from "./routes.js";
</script>

<main>
  <h3><a href="/" use:link>TravelTheWorld.com</a></h3>
  <Router {routes}/>
</main>

<style>
  @import url("https://fonts.googleapis.com/css2?family=Montserrat&display=swap");

  :global(body) {
    margin: 0;
    padding: 20px;
  }

  :global(a) {
    text-decoration: none;
    color: #551a8b;
  }

  main {
    max-width: 800px;
    margin: 0 auto;
    font-family: "Montserrat", sans-serif;
  }
</style>

We imported Router itself, and the link component from svelte-spa-router package, and then the routes object we created earlier ourselves.

We then included an h3 Home route (/) that will be visible in all paths (just so that the user can access the Home page from anywhere) and then we included the Router component what decides what gets rendered based on the URL that is active.

For the styling, we created a couple of global style rules. For the body, we reset the margin so it looks the same on all browsers, as well as added some padding so it looks nice on the responsive screens. For the link elements we removed all the decoration rules and set a common color.

Finally, for the main wrapper we set the max-width, centered it horizontally, and set the Montserrat font for the text of the articles.

Testing the app

First, check if your development server is still running in your terminal. If it is not run the npm run dev command and navigate to localhost:5000 in your browser, where you should be presented with the landing view of the blog.

Home app

This is the Router already in action, matching the / route to the Home component that is looping through the blogs array and using the Card component to display all the articles.

Now click on any of the articles on the homepage. Depending on which article you clicked, you should now be presented with a separate view for that particular article itself.

Article opened

Notice the URL changed from / to something like /#/article/17-awesome-places-to-visit-in-germany and the app did not refresh during the request.

Now, copy the URL, open the new tab in your browser, paste it in and execute. You will be presented with the same view you saw in the previous tab.

Finally, let's test for the non-existing routes. Change the URL to anything random, say like /#/random or /#/article/random and execute.

Not Found

Notice you got presented with the custom error screen. You can use this as a fallback for all the non-existent links if some of the articles are getting removed, for example.

Congratulations, great job on following along! All the above tests returned the expected behavior, meaning our SPA router is working as expected.

Conclusion

In this application, we learned all of the basic routing functions you would need for your single-page applications: to create static routes, create routes with parameters and make wildcards to handle non-existing routes.

Feel free to expand the application by adding new components and assigning them to new routes. If you are planning to scale the application, I would recommend using a content management system (CMS) or a separate database and authentication system.

Finally, the svelte-spa-router is open source on GitHub, so make sure to check it out and contribute your own ideas and improvements to make it even better for future users.