In the last article, we briefly looked at spinning up Svelte. This article doesn't require you to have done this but if you don't have a Svelte installation I advise following the quick start guide on the Svelte website.
There are several ways we can look at routing in Svelte, the first choice may be Sapper (currently in beta) which is a mini-framework built by the Svelte team. There are also a couple of packages that you can use to integrate routing such as svelte-routing or Page.js.
For this article, we are going to be utilising the awesome library Page.js. We have chosen Page.js as it offers a lot of granular control over our individual routes and works off any anchor (<a>
) links in our site without requiring any custom components. I find this works a lot better as when content is rendered from a WYSIWYG or Markdown files all of the internal anchor links will work flawlessly.
Setting Up Our Environment
To start, we need to install Page.js. Open up your terminal in the root directory of your site and run the following:
yarn add page # or npm install page
We will also need to make a tweak to our package.json file to ensure that if we reload on a page we have navigated to, for example, /blog, we will not receive a server error and it will load the appropriate route. Open up your package.json and edit the following line:
"start": "sirv public"
To append the --single
option, as so.
"start": "sirv public --single"
This should set up everything so we bump into as few errors as possible later on.
Writing the Router
When we look at how we will integrate Page.js into Svelte, I want to explain a few bits about how things work and show a few examples. Page.js is an extremely extensible so you can perform a lot of logic on a per route basis.
Setup the Router
At the top of the App.svelte
file include the router from the Page.js package. We'll also want to create a new file for our Home route so create a new file in the following location routes/Home.svelte
.
<script>
import router from 'page'
// Include our Routes
import Home from './routes/Home.svelte'
</script>
Make sure you populate the routes/Home.svelte
file with some dummy text so you can distinguish if it has loaded the route, something like so:
<h1>Home Page</h1>
Next, back in our App.svelte
file we will need to create a couple of variables that we will pass through to the child components and tell Svelte what route to load.
<script>
...
let page
let params
</script>
To get Page.js to watch, and change the component, based on the URL we are on, we will need to call the router
package. We can do this by passing the URL as the first property and then our function (to update the component) as the second property.
// Set up the pages to watch for
router('/', () => (page = Home))
In the above code, we are watching for any requests to the root of our site at "/" and then set the page
variable to the Home
component we imported earlier from the ./routes/Home.svelte
file.
We then need to make sure the router is always watching for changes, which we can do by running router.start()
. This will look like the following:
// Set up the router to start and actively watch for changes
router.start()
Finally, to get the router to load the correct component we can utilise svelte:component
which will allow us to recreate and destroy the component on any page load (meaning that a different Route will load when you navigate to any anchors/ the browser history changes). After the closing </script>
tag add the following:
<svelte:component this="{page}" params="{params}" />
Your App.svelte
should look something similar to the following when all put together.
<script>
import router from 'page'
// Include our Routes
import Home from './routes/Home.svelte'
// Variables
let page
let params
// Set up the pages to watch for
router('/', () => (page = Home))
// Set up the router to start and actively watch for changes
router.start()
</script>
<svelte:component this="{page}" params="{params}" />
If you run yarn dev
from the root directory of your application, you should now see the Home Page load and display any content you added to the Home.svelte
file.
Expanding On Our Router
To get our router to, actually, route things, we will need to set up a couple more instances of routes and add them to our code. Create two new files in the routes directory for Blog.svelte
and SingleBlog.svelte
.
Blog.svelte
Do not worry if you do not fully understand everything that's going on here. I've included a bit of dummy code in the example for Blog.svelte
as I wanted to show how we can dynamically load content based on route parameters. To summarise what is going on - we utilise the onMount
from Svelte to fetch a list of JSON blog posts from a dummy API and then loop over them all and display a link to each item.
<script>
import { onMount } from 'svelte'
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/'
let data = []
onMount(async () => {
const response = await fetch(apiUrl)
data = await response.json()
})
</script>
<h1>Blog</h1>
{#each data as item }
<div>
<h5><a href="/blog/{item.id}">{item.title}</a></h5>
</div>
{/each}
SingleBlog.svelte
For the single blog route, we are going to lean on the params
variable we set up in App.svelte
and then fetch a single post from the dummy API with that ID.
<script>
import { onMount } from 'svelte'
export let params
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/'
let data = []
onMount(async () => {
const response = await fetch(apiUrl + params.id)
data = await response.json()
})
</script>
<h1>{data.title}</h1>
<p>{data.body}</p>
Modifying Our Original Code
Now we have the two new files created, we will need to go back and edit our App.svelte
to include them. Open the file and import the two new routes.
import Blog from './routes/Blog.svelte'
import SingleBlog from './routes/SingleBlog.svelte'
Add two new instances of the router
so that we can watch for the new pages. To watch for the user visiting /blog add the following:
router('/blog', () => (page = Blog))
When adding a dynamic route, we are presented with another property to router()
which allows us to load or manipulate any data before we pass through to the route endpoint. We will utilise this in our code so that we can ensure we pass through any query parameters to our end component.
Add the following code beneath the other instances of router()
:
router(
'/blog/:id',
// Before we set the component
(ctx, next) => {
params = ctx.params
next()
},
// Finally set the component
() => (page = SingleBlog)
)
In this code, we have another function in the middle which allows us to pass through both ctx
(context) and a next
function call. In this example, we are assigning the params
defined in our URL to the params
variable in the code before proceeding with updating the route component.
If you look back at the code for the routes/SingleBlog.svelte
file, you will see we used these params to allow us to get the ID of the post we are visiting from the remote API.
To actually be able to navigate to these routes, we need to add a bit of boilerplate to our application, update the code after the closing </script>
tag in the App.svelte
file to the following:
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
</nav>
<main>
<svelte:component this="{page}" params="{params}" />
</main>
Our App.svelte
file should now look a little like this:
<script>
import router from 'page'
// Include our Routes
import Home from './routes/Home.svelte'
import Blog from './routes/Blog.svelte'
import SingleBlog from './routes/SingleBlog.svelte'
// Variables
let page
let params
// Set up the pages to watch for
router('/', () => (page = Home))
router('/blog', () => (page = Blog))
router(
'/blog/:id',
(ctx, next) => {
params = ctx.params
next()
},
() => (page = SingleBlog)
)
// Set up the router to start and actively watch for changes
router.start()
</script>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
</nav>
<main>
<svelte:component this="{page}" params="{params}" />
</main>
Test it out in your browser and you should see everything working without any page reloads or errors.
Further Examples
There are lots you can do by utilising Page.js and Svelte together. Although I feel this article is getting a little lengthy, I did want to cover over a few points which may come in super handy for anyone building a more complex application.
Authentication
We can utilise the router()
to check if there is a user variable stored, or if the user is authenticated, and then if not redirect to a login page. This is super useful for creating a locked-down area.
<script>
...
import AuthedRoute from 'routes/AuthedRoute.svelte'
import Login from 'routes/Login.svelte'
let user = null // any logic you have for getting the user here, or more to come on this later.
router("/login", () => page = Login)
router('/private', () => {
// If the user is not set, redirect to login
if (! user) {
router.redirect('/login')
}
page = PrivateRoute
})
...
</script>
Catch all pages/Error Pages
What happens when a user hits a page where we don't have a route set up? Well, currently they would see a mostly blank page. We can catch this by setting up a wildcard on the router()
and show an error route instead.
<script>
...
import ErrorPage from "./routes/ErrorPage.svelte"
router('/*', () => page = Error)
...
</script>
Conclusion
There is a lot of content in this article discussing the ways to implement Page.js into Svelte to create a dynamic router and I hope it has been useful. The way we have structured our document is not the cleanest at the moment as it will mean we have to repeat a lot of logic within our router()
calls and in the main App.svelete
file. In a future article, I want to talk about how we can refactor this and set up the routes a bit more efficiently.