Effortlessly Nuxt navigation: Crafting Dynamic breadcrumbs with Storefront UI

May 05, 2023 ยท 9 min read
Effortlessly Nuxt navigation: Crafting Dynamic breadcrumbs with Storefront UI

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

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.

The levels of breadcrumbs

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 with pages\index.vue as its view component. Its name will be index, the same as the file name.
  • \products with pages\products\index.vue and has products as its name.
  • \products\details for pages\products\details view, with products-details as its name, since details is nested inside products.

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:

  1. Watch for the route's changes, specifically on name, path, and meta properties instead of the whole route object.
  2. Initialize a default value for our breadcrumbs array as the Home route.
  3. Trigger the watcher immediately so we will also have the breadcrumbs calculated on the initial page load or page refresh.
  4. Pass breadcrumbs as the return value for the composable.
  5. 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:

  1. Get the list of the available routes from the router instance of useRouter().
  2. We will find the matching route for each breadcrumb's path.
  3. 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.

The Breadcrumbs code in Storefront UI doc site

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:

The screenshot of breadcrumbs component in nested page

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:

The screenshot of formatted breadcrumbs component using meta field

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 compared to approaches like Regex 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 ๐Ÿ‘‡๐Ÿผ ๐Ÿ˜‰