Using breadcrumbs can be beneficial for a complex site since it provides users a clear visual understanding of the site hierarchy and a more straightforward approach for a user to retrace their steps, improving their experience and engagement while reducing the bouncing rates for our sites.
In this post, we will discover the step-by-step process of creating an intuitive Breadcrumbs component with Nuxt and Storefront UI in our comprehensive guide.
Table of Content
- Table of Content
- The importance of breadcrumbs in web navigation
- Prerequisites
- Understanding the file-based routing system
- Creating a useBreadcrumbs composable
- Integrating with the UI component
- Summary
The importance of breadcrumbs in web navigation
Breadcrumbs trail displays the current location within a web application in the context of the application's hierarchy and allows users to navigate back to previous pages or levels easily. It offers an alternative visual presentation for assisting navigation, often a horizontal sequence of text links separated by a >
symbol to indicate the nested level of the text that comes after it.
You can view breadcrumbs like a file directory hierarchy, where the root appears first and the current page link appears last.
There are three types of breadcrumbs: location, attribute, and path based. This post will discuss implementing a path-based Breadcrumbs component for our Nuxt application using its routing mechanism and Storefront UI for the visual display.
Note: For a Vue application with Vue Router installed, you can easily reuse the discussed code in this post with a little bit of modification.
Prerequisites
It would help if you had a Nuxt application set up and ready to go by using the following command:
npx nuxi init breadcrumbs-demo
Which breadcrumbs-demo
is the name of our code demo application. You can change to any name per your preference.
Next, let's add Storefront UI to our project by installing its package and the TailwindCSS module for Nuxt with the following command:
yarn add -D @nuxtjs/tailwindcss @storefront-ui/vue
You should follow the Storefront UI instructions to set up TailwindCSS and Storefront UI together in the Nuxt product. Once done, we are ready for our tutorial!
Understanding the file-based routing system
Nuxt uses Vue Router and a file-based system to generate the application's routes from the files in the /pages
directory. For example, if you have the following file structure:
\page
|--index.vue
|--\products
|-----index.vue
|-----details.vue
Behind the scene, the Nuxt engine will automatically build the following routes:
-
\
as home page withpages\index.vue
as its view component. Its name will beindex
, the same as the file name. -
\products
withpages\products\index.vue
and hasproducts
as its name. -
\products\details
forpages\products\details
view, withproducts-details
as its name, sincedetails
is nested insideproducts
.
You can view the routes using the router instance's getRoutes()
method. You can also name a route as dynamic using the []
syntax, such as [sku].vue
. In this case, Nuxt will generate the dynamic path as /:sku()
, which :sku()
is the Regex pattern Vue Router will use to match and extract the target params to sku
field when relevant.
Great. Now we understand how the routing system works in Nuxt. We can build our breadcrumbs mechanism, breaking down the URL into crumbs.
Creating a useBreadcrumbs composable
To construct our breadcrumbs, we create a useBreadcrumbs()
composable, where we perform the following actions:
- Watch for the route's changes, specifically on
name
,path
, andmeta
properties instead of the wholeroute
object. - Initialize a default value for our
breadcrumbs
array as the Home route. - Trigger the watcher immediately so we will also have the breadcrumbs calculated on the initial page load or page refresh.
- Pass
breadcrumbs
as the return value for the composable. - We only trigger watchers when the current route is not on the Home page.
The essential code for useBreadcrumbs()
is as follows:
export const useBreadcrumbs = () => {
const route = useRoute()
const HOMEPAGE = { name: 'Home', path: '/' };
const breadcrumbs:Ref<Array<{ name: string; path: string; }>> = ref([ HOMEPAGE ])
watch(() => ({
path: route.path,
name: route.name,
meta: route.meta,
matched: route.matched,
}), (route) => {
if (route.path === '/') return;
//TODO - generate the breadcrumbs
}, {
immediate: true,
})
return {
breadcrumbs
}
}
Next, we will implement how to compute the breadcrumbs from the current route, including dynamic and nested ways.
Handling dynamic and nested routes
To construct a page's breadcrumbs, it's always better to have the current route know who its parent is. However, in Vue Router and Nuxt, unfortunately, there isn't a way to do so. Instead, we can construct the breadcrumbs' paths by recursively slicing the current breadcrumb's path by the last index of the symbol /
.
Take our /products/about/keychain
path, for instance. We will break it down into the following paths: "/products/about/keychain"
, "/products/about"
, "/products"
, and ""
. Each path is a breadcrumb we need to display. And to get the display name for these breadcrumbs, we need to do the following:
- Get the list of the available routes from the
router
instance ofuseRouter()
. - We will find the matching route for each breadcrumb's path.
- Our stop condition is when the reduced path is the Home page.
Our code for getBreadcrumbs()
looks as follows:
function getBreadcrumbs(currPath: string): any[] {
//1. When we reach the root, return the array with the Home route
if (currPath === '') return [ HOMEPAGE ];
//2. Continue building the breadcrumb for the parent's path
const parentRoutes = getBreadcrumbs(currPath.slice(0, currPath.lastIndexOf('/')));
//3. Get the matching route object
//TODO
//4. Return the merged array with the new matching route
return [
...parentRoutes,
{
path: currPath,
//TODO
name: currPath,
}
]
}
We currently return the currPath
as the path
and name
for the breadcrumb. Still, we must implement how to detect the matching route based on the generated route configurations from Nuxt, including dynamic routes and dynamic nested routes. Let's do that next.
Matching the route's pattern
When working with matching routes' paths, there are many scenarios related to dynamic routes we need to handle, including:
- Dynamic routes such as
/products/:id()
- Dynamic route and static route under the same parent, such as
/products/:id()
(pages/produts/[id].vue
) and/products/about
(pages/products/about.vue
) - Dynamic route nested in another dynamic route, such as
/products/:id()/:field()
The most straightforward approach is to split the route's and current paths into parts by the separator /
. Then we iterate the elements and compare one to one to see if it is the same value or if the subpath starts with :
as shown in the following code for isMathPatternPath
:
const isMathPatternPath = (pathA: string, pathB: string) => {
const partsA = pathA.split('/');
const partsB = pathB.split('/');
if (partsA.length !== partsB.length) return false;
const isMatch = partsA.every((part: string, i: number) => {
return part === partsB[i] || part.startsWith(':');
})
return isMatch;
}
We then use isMathPatternPath
in our getBreadcrumbs()
function on the currPath
, and receive an array of the matched route(s) as a result with the following assumptions:
- If there is a static route and a dynamic route resides on the same parent, it will be a match for both.
- The static routes will always appear before the dynamic routes in such a matched routes array (letters appear before symbols like ':')
- The matched array contains more than one result for a static route with a dynamic sibling. In this case, we will take the exact match using the
===
comparison. Otherwise, the array should contain a single result.
And thus, our implementation for getBreadcrumbs()
will be as follows:
function getBreadcrumbs(currPath: string): any[] {
//1. When we reach the root, return the array with Home route
if (currPath === '') return [ HOMEPAGE ];
//2. Continue building the breadcrumb for the parent's path
const parentRoutes = getBreadcrumbs(currPath.slice(0, currPath.lastIndexOf('/')));
//3. Get the matching route object
const founds = routes.filter(r => isMathPatternPath(r.path, currPath));
const matchRoute = founds.length > 1 ? founds.find(r => r.path === currPath) : founds[0];
//4. Return the merged array with the new matching route
return [
...parentRoutes,
{
path: currPath,
//TODO
name: matchRoute?.meta?.breadcrumb || matchRoute?.name || matchRoute?.path || currPath,
}
]
}
Based on the matchRoute
, we will use the meta.breadcrumb
field to get the desired name for displaying, or its name, path or currPath
as the fallback value.
And we can update our useBreadcrumbs()
composable with the following code:
export const useBreadcrumbs = () => {
//...
watch(() => ({
path: route.path,
name: route.name,
meta: route.meta,
matched: route.matched,
}), (route) => {
if (route.path === '/') return;
breadcrumbs.value = getBreadcrumbs(route.path);
}, {
immediate: true,
})
//...
}
With that, our useBreadcrumbs()
is ready to use. Let's display it!
Integrating with the UI component
We will copy the code of Storefront UI Breadcrumbs with a Home icon, and paste in our components/Breadcrumbs.vue
.
In the script setup
section, we will change the breadcrumbs
to props, as follows:
const props = defineProps({
breadcrumbs: {
type: Array,
required: true,
},
});
The sample code comes with each breadcrumb having a name
and a link
. Hence we need to look for item.link
and replace them with item.path
in the template
section. Also, we want to render SfLink
as a NuxtLink
to avoid full page reload by adding :tag="NuxtLink"
to every SfLink
appears in the template
, and the following to the script
section:
import { resolveComponent } from 'vue';
const NuxtLink = resolveComponent('NuxtLink');
Great. Our Breadcrumbs
component is complete.
Now in /layouts/default.vue
, we will get the breadcrumbs
from useBreadcrumbs()
composable and pass it to Breadcrumbs
component for rendering, as below:
<template>
<Breadcrumbs class="mt-4 ml-4" :breadcrumbs="breadcrumbs"/>
<div class="h-px m-4 bg-neutral-200 divider"></div>
<slot />
</template>
<script setup>
import { useBreadcrumbs } from '../composables/useBreadcrumbs';
const { breadcrumbs } = useBreadcrumbs();
</script>
Finally, make sure you have the following code in your app.vue
:
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
And that's all it takes. When we navigate to a page, the app will display the corresponding breadcrumbs:
Using the meta field for breadcrumbs
As you notice, the current breadcrumbs use the default name defined in the matched route, which is only sometimes readable. In our implementation, we have used route.meta?.breadcrumb
as the breadcrumb's name. To define meta.breadcrumb
, we will use Nuxt's built-in method definePageMeta
as in the following example:
<script setup>
/**pages/products/index.vue */
definePageMeta({
breadcrumb: 'Products Gallery',
})
</script>
On the build time, Nuxt will merge the desired page's meta into the route's meta
, and we will have the breadcrumbs displayed accordingly:
Note that you can't define the meta following the above approach for the dynamic route. Instead, in useBreadcrumbs
, you can watch the route.params
and get the appropriate name from the params and the relevant data, such as a product's title.
Summary
You can find the working code here.
In this post, we have explored how to craft a breadcrumbs mechanism for our Nuxt application using its built-in router and visualize them with a Breadcrumbs component using Storefront UI. The implementation is straightforward and may not be optimal in a more complex routing system with multiple layers of nesting dynamic and static routes. However, it is a good starting point for you to build your own breadcrumbs system, and remember, KISS rules!
I hope you find this post helpful. If you have any questions or suggestions, please leave a comment below. I'd love to hear from you.
👉 If you'd like to catch up with me sometimes, follow me on Twitter | Facebook.
👉 Learn about Vue with my new book Learning Vue. The early release is available now!
Like this post or find it helpful? Share it 👇🏼 😉
Top comments (0)