2019/12/11

Setting up Routing In Svelte with Page.js

Svelte

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.