Migration Guide from Routify to SvelteKit Router
What is the need for migration?
I had a few personal projects created using Svelte which I am now migrating to SvelteKit.
Svelte did not provide any out-of-box navigation mechanism and hence I was forced to look out for third-party libraries.
SvelteKit, on the other hand, has a built-in mechanism for routing and is constantly evolving for good. Since we have a built-in solution available now, it makes sense to adhere to it.
Before we proceed ahead, I want to stress on this point that Routify is an excellent library. I have used that across many personal and professional projects and the experience has been awesome. The only reason I am migrating from Routify to SvelteKit's built-in router is just my personal opinion which is - I installed Routify only because Svelte didn't have any routing mechanism but SvelteKit does.
Does Routify support SvelteKit?
The current version of Routify i.e. v2.18 DOES NOT support SvelteKit.
We will need to upgrade that to v3-next and as the name suggests, that is still in beta state.
Just upgrading the package will not be sufficient, we will need to do a couple of more configurations that are specified in their documentation. I am sure when they release a stable v3 version, they will have a migration guide (from v2 to v3) ready by then.
Process of Migration
Now that we are done with the introduction, let's go through the migration process.
Rearrange folder structure
In Routify, all the pages are placed within the
pages
directory but in SvelteKit, they have to be placed within theroutes
directory.In SvelteKit, we do not have the facility to reset the layout, the only way to do that is to group the paths with specific layouts. Let's understand this with an example, consider this project structure:
src routes (reset) folders_with_different_layouts +layout.svelte (main) rest_of_folders +layout.svelte +layout.svelte +page.svelte +error.svelte
The root layout file i.e. src/routes/+layout.svelte will be inherited by all the routes no matter what. So this file should contain the bare minimum logic (like just an empty file having only
<slot />
)or the code that will be used by all the pages.The next step will be to create two groups - (reset): that will have a layout that is different from the root one. (app): that will have a layout for pages that does not requires a total reset.
Rename Files
ROUTIFY | SVELTEKIT |
_folder.svelte or _layout.svelte | +layout.svelte |
index.svelte | +page.svelte |
_reset.svelte | Not possible as discussed in the previous section |
_fallback.svelte | Needs special handling as discussed in the next section |
Special Handling of Error pages
In Routify, we have _fallback.svelte file which acts as a "catch" for 404 URLs. We can have fallback at the root and per route basis as well.
In SvelteKit, this is not possible. All the 404 are handled by the root error page i.e. src/routes/+error.svelte. So the logic must be adjusted to handle 404 at the root level than that of the per-route level.
If we must need to have an error page at the route level, we will have to create a corresponding load function in layout.ts or layout.server.ts at that route level and throw an error from there. You can read more on this in SvelteKit's official documentation.
Update Utilities
Routify provides tons of helper functions and on the contrary SvelteKit provides just a few. So we need to adjust the logic accordingly, create new helper functions etc to main the routing consistency. In this section, I will be discussing 9 such utilities/helper methods that I have extensively used in my projects. Based on that, feel free to extend logic for other methods as well.
goto()
SvelteKit has a built-in method
goto()
. We have to do 2 changes here is:change imports from
import { goto } from '@roxi/routify';
toimport { goto } from '$app/navigation';
rename
$goto()
togoto()
One thing that we have to take care of is the way we pass parameters in
goto()
. In Routify, we have,$goto('url', { ...params } )
& in SvelteKit, we have to pass params as query-parametersgoto('url?a=b&c=d')
Here is the utility method that I have created which is in sync with Routify:
import { goto as _goto } from '$app/navigation'; ... export function goto(url: string, params?: { [key: string]: string }) { if (params && Object.keys(params).length > 0) { url = url.includes('?') ? url + '&' : url + '?'; for (const [key, value] of Object.entries(params)) { url += key + '=' + value + '&'; } } if (url.endsWith('&')) { url = url.substring(0, url.length - 1); } _goto(url); } // USAGE goto('url') goto('url', { a: 'b', c: 'd' })
isActive()
SvelteKit does not provide a utility to check if the link is active or not, we have to write our custom logic for that, which is quite simple using SvelteKit's store. Example:
{#each menu as item} <li> <a href={item.link} class:active={$page.url.pathname === item.link}>{item.title}</a> </li> {/each}
Alternatively, I have written a helper function for the same. The only caution we have to take is to pass the value of the
path
properly.export function isActive(page: Page<Record<string, string>>, path: string) { const pathname = page.url.pathname; return pathname === path; } // invocation const currentUrl = new URL(window.location.href); <a href={url($page, item.link)} class:active={isActive($page, item.link)}>{item.title}</a>
url()
Before we proceed ahead. let's have a walkthrough on how the navigation works in Routify and SvelteKit. Assume the following folder structure:
pages profile user index.svelte <- We are at this page index.svelte index.svelte <- We want to navigate here
Navigating from
/profile/user
to/profile
:In Routify:
<a href={$url('../')}>To Profile Page</a>
In SvelteKit:
<a href='/profile'>To Profile Page</a>
Navigating from
/profile/user
to/
:In Routify:
<a href={$url('../../')}>To Home Page</a>
In SvelteKit:
<a href='/'>To Home Page</a>
From the above example, it is evident that in Routify when we want to navigate from page-A to page-B, page-A will be considered as the root page & navigation path should be calculated from that node. Whereas in SvelteKit, src/routes/+page.svelte is always considered as the root page and navigation will be calculated from that node.
Here is the utility method for the same:
export function url(page: Page<Record<string, string>>, path: string) { const pathname = page.url.pathname; if (path == null) { return path; } else if (path.match(/^\.\.?\//)) { // Relative path (starts with `./` or `../`) const [, breadcrumbs, relativePath] = path.match(/^([./]+)(.*)/) as string[]; let dir = pathname.replace(/\/$/, ''); const traverse = breadcrumbs.match(/\.\.\//g) || []; // if this is a page, we want to traverse one step back to its folder traverse.forEach(() => (dir = dir.replace(/\/[^/]+\/?$/, ''))); path = `${dir}/${relativePath}`.replace(/\/$/, ''); path = path || '/'; // empty means root else if (path.match(/^\//)) { // Absolute path (starts with `/`) return path; } else { // Unknown (no named path) return path; } return path; } // USAGE <a href={url($page, item.link)} class:active={isActive($page, item.link)}>{item.title}</a>
params
Let's discuss
params
with the help of an example. Assume the following folder structure:pages [country] [currency] index.svelte <- we are here
The given URL is:
https://google.com/in/inr?a=b&c=d
In Routify,
$params
will be an object{ country: in, currency: inr, a: b, c: d }
In SvelteKit, for getting that same output, we will have to use the
$page
store. Example:$page.params
will be an object{ country: in, currency: inr }
and$page.url.searchParams('a')
will outputb
or$page.url.search
will give us?a=b&c=d
Alternatively, I have written a helper function:
export function getParams(page: Page<Record<string, string>, string | null>) { let returnValue = {}; const optionalParams = page.params; if (Object.keys(optionalParams).length > 0) { returnValue = { ...returnValue, ...optionalParams }; } const searchParams = page.url.search; if (searchParams.length > 1) { const temp = Object.fromEntries(new URLSearchParams(searchParams)); returnValue = { ...returnValue, ...temp }; } return returnValue; }
To invoke this function:
import { page } from "$app/stores"; import { getParams } from "$lib/utils/router-helper"; const params = getParams(page);
afterPageLoad()
- SvelteKit has a built-in method
afterNavigate
which we can import from$app/navigation
to replace Routify's$afterPageLoad(...)
- SvelteKit has a built-in method
beforeUrlChange()
- SvelteKit has a built-in method
beforeNavigate
which we can import from$app/navigation
to replace Routify's$beforeUrlChange(...)
- SvelteKit has a built-in method
redirect()
SvelteKit has a built-in method
redirect
which we can import from@sveltejs/kit
to replace Routify's$redirect(...)
One thing to keep in mind is, Routify's
$routify("URL")
just has one parameter which is the URL to be redirected to, whereas, in SvelteKit, we will have to provide HTTP status as well, For example,redirect(302, "URL")
$page
SvelteKit has equivalent
$page
store variable.In Routify, we mostly use the
$page
to access meta, title and parent property. Since we don't have these properties with the$page
of SvelteKit, we will have to adjust the logic in those files.title
corresponds to the last fragment of the URL, i.e., if URL =https://www.google.com/settings
, title = "settings".meta
is something we will have to fetch from the corresponding layout.ts or layout.server.ts file which I have discussed briefly in the next section.parent
is something we won't be able to compute. Routify maintains a tree hierarchy behind the scenes and such a feature is not available in SvelteKit, so we will have to rewrite the logic in those files.
$layout
$layout
, like$page
is used to accesschildren
andmeta
properties. We don't have any equivalent functionality in SvelteKit.I have discussed briefly
meta
in the next section, now I will walk you through the process of fetchingchildren
of the given layout. Make sure that this snippet is used in +layout.svelte files only:export function getLayoutChildren( routeId: string, modules: Record<string, () => Promise<unknown>> ) { let returnValue: Array<{ path: string; title: string }> = []; let root = '/'; // remove group layout from path const removeGroupLayouts = (path: string): string => { let newPath = path; if (path.includes('(')) { newPath = newPath.substring(0, newPath.indexOf('(')) + newPath.substring(newPath.indexOf(')/') + 1); } if (newPath.includes('(')) { return removeGroupLayouts(newPath); } return newPath.replaceAll('//', '/'); }; // append layout (file) name to the path const rootLayout = (path: string): string => { if (routeId.includes(path)) { // return difference return routeId.split(path).join(''); } return '/'; }; routeId = removeGroupLayouts(routeId); for (const [key, value] of Object.entries(modules)) { const keyStartIndex = key.indexOf('./') + 1; const keyEndIndex = key.indexOf('/+page'); if (keyEndIndex > keyStartIndex) { let title = key.substring(keyStartIndex + 1, keyEndIndex); if (routeId.length > 1 && root.length < 2) { // fetch root layout to be appended with path root = rootLayout(title); } if (title.includes('/')) { // in case of optional params /[country]/[language] title = title.substring(title.lastIndexOf('/') + 1); } const tempValue = removeGroupLayouts(value.name); const valueEndIndex = tempValue.indexOf('/+page'); const path = tempValue.substring(2, valueEndIndex); returnValue = [...returnValue, { path, title }]; } } // append layout to the path if (root.length > 0) { returnValue = returnValue.map((value) => { const appendValue = root === '/' && routeId.length > 2 ? routeId + '/' : root; return { path: appendValue + value.path, title: value.title }; }); } return returnValue; }
Invoking this function only with +layout.svelte files:
<script lang="ts"> import { page } from '$app/stores'; import { getLayoutChildren } from '$lib/utility/router-helper'; import { onMount } from 'svelte'; onMount(() => { const modules = import.meta.glob('./**/+page.svelte'); const children = getLayoutChildren($page.route.id as string, modules); }) </script>
The output will be something like the below for the project structure:
src routes profile admin +page.svelte user +page.svelte +page.svelte +layout.svelte <- invoked here
leftover
$leftover
is only used in _fallback.svelte files (in Routify whose equivalent is +error.svelte in SvelteKit). The value of$leftover
is the unused part in the URL.Unfortunately, we don't have any such functionality in SvelteKit and the rough part is that we won't be able to create a utility/helper for this due to the reason SvelteKit handles 404 pages.
Assume the following folder structure:
src pages admin user index.svelte _fallback.svelte index.svelte _fallback.svelte index.svelte _fallback.svelte
If we enter the URL
/admin/user/foo-bar
, in Routify, _fallback within the user will be invoked and the value of$leftover
will be "foo-bar". Similarly, for URL/admin/foo-bar
, _fallback within admin will be invoked and the value of$leftover
will be "foo-bar" and so on...In SvelteKit, if 404 occurs, control always goes to src/routes/+error.svelte, there is no concept of redirecting users to specific error pages in 404 scenarios and hence implementing a helper/utility for this feature is not possible.
The closest possible solution that I can think of is the rest parameters in routing. We can configure our route as
src/routes/admin/[...leftover]/+page.svelte
With this approach we can handle the request per route basis but again as said before this is the closest solution and not the exact one. If you come across any hack, do let me know in the comments section!
Breadcrumb and Dynamic Navigation
In Routify, to generate the breadcrumbs and a Navigation bar, we have traversed the $page.parent
and $layout.children
in a recursive manner. As this is not possible in SvelteKit, I have written a blog post showcasing how to generate breadcrumbs and dynamic navigation.
Meta Tags
Finally coming to the last section :) that is Meta tags. In Routify, we specify meta options as
<!--routify:options key=value -->
and access them in CLOSEST layout or page file as$layout.meta
or$page.meta
In SvelteKit, we can export an object from either layout.ts, layout.server.ts, page.ts, or page.server.ts and access them in the CLOSEST layout or page file. Example:
// file layout.ts, layout.server.ts, page.ts, or page.server.ts return { title: 'Hello World', category: 'Blog', }
Access the meta values in the CLOSEST +layout.svelte or +page.svelte file as:
export let data; const category = data.category; // outputs "Blog"
We can also fetch metadata from parent routes:
export const load = (async ({ data }) => { return { ...data, parentCategory: 'From Parent' } }) satisfies LayoutLoad;
Wrapping Up
That's all folks! If you have any ideas/suggestions to add, do let me know in the comments section!