📅 3 years ago 🕒 13 min read 🙎♂️ by Madza
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:
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.