DEV Community

Cover image for The Perfect Breadcrumbs (in Nuxt)
@lukeocodes 🕹👨‍💻
@lukeocodes 🕹👨‍💻

Posted on • Edited on

The Perfect Breadcrumbs (in Nuxt)

Breadcrumbs can be a bit of a pain. But there are lots of reasons you might want to make them.

TL;DR: I made a self-contained component that builds semantic breadcrumbs based on the path to the file using the router. It matches the paths against the router before outputting links.

The gist is at the end of the post.

What Are Breadcrumbs and Do They Really Deserve This Much Attention?

Breadcrumbs most commonly appear at the top of a page as text links denoting the path to the post (or back to the index). Breadcrumbs are an important navigation mechanism.

Combined with structured data or semantic markup like RDFa, they also act as an important SEO tool, for sites such as Google to understand the structure of your site.

Showing breadcrumbs on Vonage.com

When Google finds the data it needs, it can display the site structure in results.

Showing Vonage.com breadcrumbs in a Google result

Why Make This?

Most of the examples I found online take an array from the page you're placing the breadcrumbs. This works from the / divided path but skips paths that the router cannot match.

How Does It Work?

I'll focus on the JS rather than the JSX. You'll likely make better markup for it than I would.

Starting with an empty output.



export default {
  computed: {
    crumbs() {
      const crumbs = []

      return crumbs
    },
  },
}


Enter fullscreen mode Exit fullscreen mode

Now, we'll get the current full path.



export default {
  computed: {
    crumbs() {
      const fullPath = this.$route.fullPath
      const params = fullPath.substring(1).split('/')
      const crumbs = []

      console.log(params) 

      // url:     /blog/2020/11/20/my-post-url
      // outputs: ['blog','2020','11','20','my-post-url']

      return crumbs
    },
  },
}


Enter fullscreen mode Exit fullscreen mode

Next, recompile the URL bit by bit.



export default {
  computed: {
    crumbs() {
      const fullPath = this.$route.fullPath
      const params = fullPath.substring(1).split('/')
      const crumbs = []

      let path = ''

      params.forEach((param, index) => {
        path = `${path}/${param}`

        console.log(path)

      })

      // outputs: /blog
      //          /blog/2020
      //          /blog/2020/11
      //          /blog/2020/11/20
      //          /blog/2020/11/20/my-post-url

      return crumbs
    },
  },
}


Enter fullscreen mode Exit fullscreen mode

Now, match each route on the router.



export default {
  computed: {
    crumbs() {
      const fullPath = this.$route.fullPath
      const params = fullPath.substring(1).split('/')
      const crumbs = []

      let path = ''

      // test path
      params.push('fake')

      params.forEach((param, index) => {
        path = `${path}/${param}`
        const match = this.$router.match(path)

        if (match.name !== null) {
          console.log(`yep:  ${path}`)
        } else {
          console.log(`nope: ${path}`)
        }
      })

      // outputs: yep:  /blog
      //          yep:  /blog/2020
      //          yep:  /blog/2020/11
      //          yep:  /blog/2020/11/20
      //          yep:  /blog/2020/11/20/my-post-url
      //          nope: /blog/2020/11/20/my-post-url/fake

      return crumbs
    },
  },
}


Enter fullscreen mode Exit fullscreen mode

Finally, capture only matches.



export default {
  computed: {
    crumbs() {
      const fullPath = this.$route.fullPath
      const params = fullPath.substring(1).split('/')
      const crumbs = []

      let path = ''

      params.forEach((param, index) => {
        path = `${path}/${param}`
        const match = this.$router.match(path)

        if (match.name !== null) {
          crumbs.push(match)
        }
      })

      return crumbs
    },
  },
}


Enter fullscreen mode Exit fullscreen mode

In mine, I turn the param into a title using ap-style-title-case. I have a prop that I let folks override the autogenerated page title for blog posts where the slug might not perfectly turn back into a title.



const titleCase = require('ap-style-title-case')

export default {
  props: {
    title: {
      type: String,
      default: null,
    },
  },

  computed: {
    crumbs() {
      const fullPath = this.$route.fullPath
      const params = fullPath.startsWith('/')
        ? fullPath.substring(1).split('/')
        : fullPath.split('/')
      const crumbs = []

      let path = ''

      params.forEach((param, index) => {
        path = `${path}/${param}`
        const match = this.$router.match(path)

        if (match.name !== null) {
          crumbs.push({
            title: titleCase(param.replace(/-/g, ' ')),
            ...match,
          })
        }
      })

      return crumbs
    },
  },
}


Enter fullscreen mode Exit fullscreen mode

The Full Code

Check out the gist for the whole component!

Top comments (4)

Collapse
 
bastianhilton profile image
Sebastian hilton
I took it a step further for my cms i'm building by adding a dynamic page title underneath my breadcrumbs by adding your code like this:

<h2 v-for="(crumb, index) in crumbs" :key="index" property="itemListElement" typeof="ListItem" class="breadcrumb-item pageTitle">{{$route.fullPath === crumb.path && title !== null ? title : crumb.title}}</h2>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
irveloper profile image
Irving Caamal

Hi, this was very helpful for me, I add this i18Key: titleCase(param.replace(/\//g, '')), on the push and now you can translate the name of the page if y ou are using nuxti18n

Thanks :)

Collapse
 
retroriff profile image
retroriff • Edited

Element <meta> is not permitted as content in <li>

Collapse
 
emanuilgerganov profile image
EmanuilGerganov • Edited

Super explanation and it's working fantastic, thank you, bro. I got a new follower ;)