Madars Bišs Madars Bišs (aka Madza) is a technical writer. In his spare time, he loves to explore new topics and contribute to open-source web development.

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

8 min read 2467

Svelte Logo Over a Black Background

The two main design patterns for web apps today are multipage applications (MPAs) and single-page applications (SPAs). Each comes with significant differences in their lifecycles.

MPAs reload the entire page every time there is a request for new data. In SPAs, pages never reload, as all the static files load on the initial load, and only fetched data updates in the view when necessary.

MPAs vs SPAs

SPAs are usually faster than multipage approaches and they improve the UX significantly. However, their dynamic behavior also comes with a drawback. Because the state of the app is not assigned to the URL, it’s challenging to retrieve the view on the next load.

In this article, we will create a single-page application in Svelte and implement a routing mechanism with svelte-spa-router, which is developed and maintained by Alessandro Segala and 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 demo of the final project.

Why use 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 SPA lives in the App.svelte file, the URL https://mywebsite.com/#/profile might 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 will always require a server.

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



Setting up the Svelte 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. Install all the packages by running npm install.

After installing the packages, start the development server by running npm run dev.

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

Hello World in Svelte

We will use the svelte-spa-router package (60.9KB 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 articles and timeago.js, which helps 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 to the Svelte project

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

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


More great articles from LogRocket:


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 Traveler 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 import the array in any file of the app and use its data when necessary.

Creating components

Next, create a new folder called 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 a pointer when hovering over the image and made it open in a new tab once clicked. We also made the layout switch to a one-column layout, and the image takes all the available width of the parent when the width of the viewport is 600px or less.

Next, open 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 the urlSlug helper to create URL slugs from article titles, format to measure the time that has passed since posting, the Card component we just created, and the blogs data array. Then we looped through each post by providing necessary props for Card.

Now, 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 the 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, 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 the width of the parent and made the sides of the text to be justified.

Finally, open 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>

We will use the NotFound component 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

In svelte-spa-router, the routes are defined as objects, comprising keys for the routes and values for the components. We will purposely build a router to cover all 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 — /about, about-us, /contact, and many more based on your needs.

Next, 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. You can even chain multiple parameters: /article/:date/:author.

Finally, the user can use wildcards to control all the routes. We will use a wildcard * to catch all the nonexistent routes, displaying a NotFound view for the user. 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
};

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 a wildcard last.

Using the Svelte router in the app

If you 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 straightforward.

Open App.svelte 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 the Router itself and the link component from the svelte-spa-router package, as well as the routes object we created earlier ourselves.

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

To style, we created a couple of global style rules. For 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 Svelte app

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

Traveling Tips

This is the Router 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 be presented with a separate view for that particular article itself.

Awesome Places in Germany

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

Copy the URL, open the new tab in your browser, paste it in, and execute. You’ll see the same view you saw in the previous tab.

Finally, let’s test for the nonexisting routes. Change the URL to anything random, say /#/random or /#/article/random, and execute.

We Are Sorry

You should see a custom error screen. You can use this as a fallback for all the nonexistent 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 works as expected.

Conclusion

In this blog post, we learned all 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 nonexistent routes.

You can 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 CMS or a separate database and authentication system.

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

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Madars Bišs Madars Bišs (aka Madza) is a technical writer. In his spare time, he loves to explore new topics and contribute to open-source web development.

2 Replies to “Build a single-page application in Svelte with svelte-spa-router”

  1. good example.

    But I need a proccess in app.svelte to run

    let NODE_ENV = ‘production’
    window.process = {
    env: { NODE_ENV },
    …window.process
    };

Leave a Reply