The Perfect Breadcrumbs (in Nuxt)

@lukeocodes 🕹👨‍💻 - Nov 10 '20 - - Dev Community

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . .