Generate Breadcrumb and Navigation in SvelteKit
Table of contents
Building Dynamic Navigation
As we know that in SvelteKit, +page.svelte
files point to the actual web page. So, to generate navigation, we need to figure out a way to get the list of all +page.svelte
files. Currently, SvelteKit (v1.20.2) does not have any built-in mechanism to give us a list of all pages. The solution for this is to import all the +page.svelte
files from src/routes
using Vite's Glob imports and generate a list out of it.
Here is how we can do that - In src/routes/+layout.svelte file, we fetch all the +page.svelte
files using Vite. The output is in the form of an object with key-value pairs where the key is the path to +page.svelte
files and value is a Promise object for dynamically importing modules.
We will pass this object to our Navigation component that will perform a series of operations to generate navigation.
<script lang="ts">
import Navigation from '$lib/components/Navigation.svelte';
const modules = import.meta.glob('./**/+page.svelte');
</script>
<Navigation { modules } />
Let's focus on src/lib/components/Navigation.svelte file:
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
export let modules: Record<string, () => Promise<unknown>>;
let menu: Array<{ link: string; title: string }> = [];
onMount(() => {
for (let path in modules) {
let pathSanitized = path.replace('.svelte', '').replace('./', '/');
// for group layouts
if (pathSanitized.includes('/(')) {
pathSanitized = pathSanitized.substring(pathSanitized.indexOf(')/') + 1);
}
// for dynamic paths -> needs more triaging
if (pathSanitized.includes('[')) {
pathSanitized = pathSanitized.replaceAll('[', '').replaceAll(']', '');
}
pathSanitized = pathSanitized.replace('/+page', '');
menu = [
...menu,
{
title: pathSanitized
? pathSanitized.substring(pathSanitized.lastIndexOf('/') + 1)
: 'home',
link: pathSanitized ? pathSanitized : '/'
}
];
}
});
</script>
<div>
<ul>
{#each menu as item}
<li>
<a href={item.link} class:active={$page.url.pathname === item.link}>{item.title}</a>
</li>
{/each}
</ul>
</div>
<style lang="scss">
.nav-list {
margin: 0 1rem;
padding: 1rem;
ul > li {
display: inline-block;
}
ul, li {
margin: 0;
padding: 0;
}
a {
padding: 1rem;
color: red;
text-decoration: none;
&.active {
color: blue;
}
}
}
</style>
This component iterates over the object and generates an array link & title.
[ { "title": "profile", "link": "/profile" }, ... ]
In this process, it ignores the group layouts and dynamic paths. Now, this step is quite opinionated, one could override logic according to their requirements. Do let me know how you plan to deal with group and dynamic paths in the comments section!
With this configuration, our Navigation Bar is ready. You can see the demo at Stackblitz. In the next section, we will work our way through Breadcrumbs!
Building Breadcrumbs
SvelteKit uses a filesystem-based router. Files named +layout.svelte
determines the layout for the current page and pages below itself in the file hierarchy. We can use SvelteKit’s store page
to determine what the current path is and pass that location to a breadcrumb component which we add to the layout.
src/routes/+layout.svelte
<Breadcrumb />
src/lib/components/Breadcrumb.svelte
<script lang="ts">
import { page } from '$app/stores';
let crumbs: Array<{ label: string, href: string }> = [];
$: {
// Remove zero-length tokens.
const tokens = $page.url.pathname.split('/').filter((t) => t !== '');
// Create { label, href } pairs for each token.
let tokenPath = '';
crumbs = tokens.map((t) => {
tokenPath += '/' + t;
t = t.charAt(0).toUpperCase() + t.slice(1);
return { label: t, href: tokenPath };
});
// Add a way to get home too.
crumbs.unshift({ label: 'Home', href: '/' });
}
</script>
<div class="breadcrumb">
{#each crumbs as c, i}
{#if i == crumbs.length - 1}
<span class="label">
{c.label}
</span>
{:else}
<a href={c.href}>{c.label}</a> >
{/if}
{/each}
</div>
<style lang="scss">
.breadcrumb {
margin: 0 1.5rem;
padding: 1rem 2rem;
a {
display: inline-block;
color: red;
padding: 0 0.5rem;
}
.label {
padding-left: 0.5rem;
color: blue;
}
}
</style>
In the above snippet, the label
corresponds to the last token of the page URL, e.g., if the page URL is /sports/cricket
, the value of the label
will be cricket. If you wish to override this, you can return an object with the label
property from the corresponding +page.ts file.
Let's see that in action - so for folder structure:
src
routes
sports
cricket
+page.svelte
+page.ts
import type { PageLoad } from "../$types";
export const load = (async () => {
return {
showBreadcrumb: true,
label: 'Cricket World Cup 2023'
};
}) satisfies PageLoad;
Accordingly, we can modify our Breadcrumb component to fetch label from $page.data
<script lang="ts">
...
let tokenPath = '';
crumbs = tokens.map((t) => {
tokenPath += '/' + t;
t = t.charAt(0).toUpperCase() + t.slice(1);
return {
label: $page.data.label || t,
href: tokenPath
};
});
...
</script>
{#if $page.data.showBreadcrumb}
...
{/if}
Wrapping Up
You can see the demo at Stackblitz.
I would like to give some credit here, the idea behind generating Navigation using Vite is from a Youtube video by Web Jeda and the idea for generating Breadcrumb using SveleKit's store is from Dean Fogarty's blog.