iTranslated by AI
Why I Stopped Generating Breadcrumbs From URLs
Recently, while building a web app with SvelteKit, I was thinking about how to implement breadcrumbs.
The most common way is something like this:
pathname.split('/').filter(Boolean)
It works for now, but it feels a bit... lackluster.
- The URL and the display name don't always match
- There are
[id]segments - Navigation definitions already exist elsewhere
So, instead of decomposing the URL, I thought, "Wouldn't it be better to make the navigation definition the source of truth?"
This article is a memo of my own organization, referencing https://zenn.dev/shamokit/articles/71d40ad83d4a46.
Expected Structure
/app/
├── hoge/
│ ├── fuga/
│ └── piyo/
└── settings/
The display names do not match the URLs.
/app/hoge/ → Hoge List
/app/hoge/fuga/ → Hoge List / Fuga Details
/app/hoge/fuga/123 → Hoge List / Fuga Details / ○○
Approach
I only did three things:
- Make the Breadcrumb component for display only
- Resolve static parts from the navigation definition
- Add dynamic parts on the page side
① Make display only for display
<script lang="ts">
type Props = {
items?: { label: string; href: string }[]
}
let { items = [] }: Props = $props()
</script>
<nav aria-label="Breadcrumb">
<ol class="flex items-center gap-1">
{#each items as item, i (`${i}-${item.href}`)}
<li class="flex items-center">
{#if i !== items.length - 1}
<a href={item.href}>{item.label}</a>
<span aria-hidden="true" class="mx-1">/</span>
{:else}
<span aria-current="page">{item.label}</span>
{/if}
</li>
{/each}
</ol>
</nav>
The Breadcrumb simply receives an array.
It shouldn't hold any logic.
Since fixing it later is a hassle, I've included the minimum required ARIA attributes.
② Resolve static parts from the nav definition
The navigation definition looks something like this:
export const navData = [
{
title: 'Hoge List',
href: '/app/hoge/',
exactPath: '/app/hoge/',
children: [
{
title: 'Fuga Details',
href: '/app/hoge/fuga/',
exactPath: '/app/hoge/fuga/'
}
]
}
]
And then, resolve it from the pathname.
export function resolveBreadcrumbs(pathname: string) {
const items: { label: string; href: string }[] = []
for (const item of navData) {
if (item.exactPath && pathname.startsWith(item.exactPath)) {
items.push({ label: item.title, href: item.href })
if (item.children) {
for (const child of item.children) {
if (child.exactPath && pathname.startsWith(child.exactPath)) {
items.push({ label: child.title, href: child.href })
}
}
}
}
}
return items
}
Instead of decomposing the URL, we traverse the navigation definition as the source of truth.
This ensures that the menu and breadcrumbs are never out of sync.
Note: This example is simplified to two levels. For deeper nesting, it would be better to resolve it recursively.
③ Add dynamic routes on the page
A page like /app/hoge/fuga/[id].
<Breadcrumb items={[...resolveBreadcrumbs(page.url.pathname), { label: data.fuga.name, href: page.url.pathname }]} />
Static parts are handled by common logic, while dynamic parts are supplemented by the page.
The responsibilities are clearly separated, which feels good.
Supplement
navData and resolveBreadcrumbs are kept in the same file and imported directly by each page.
<script lang="ts">
import { page } from '$app/state'
import Breadcrumb from '$lib/Breadcrumb.svelte'
import { resolveBreadcrumbs } from '$lib/navigation'
let items = $derived.by(() => {
return resolveBreadcrumbs(page.url.pathname)
})
</script>
<Breadcrumb {items} />
By making it reactive with $derived.by, it updates automatically during page transitions.
Since the side navigation also refers to the same navData, the menu structure and breadcrumb display always remain consistent.
Thoughts
Reflections after implementation:
- Nav-based is more straightforward than URL-based
- Separating display from logic prevents confusion
- Dynamic routes are also easy to handle
And SvelteKit makes it easy.
I appreciate being able to use $state and $derived out of the box, and the ability to reuse parent data with parent() is very convenient.
It's not a flashy implementation, but this kind of organization pays off later.
SvelteKit is great.
Discussion