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.
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.
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.
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:
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
.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
4 Replies to "Build a single-page application in Svelte with svelte-spa-router"
Why not use sveltekit..?
good example.
But I need a proccess in app.svelte to run
let NODE_ENV = ‘production’
window.process = {
env: { NODE_ENV },
…window.process
};
the demo returns error: “Function called outside component initialization”
[!] RollupError: Could not resolve “./routes.js” from “src/App.svelte”
i get this error. Any ideas? Thanks!