Tutorial

How To Update Page Title and Metadata with Vue.js and vue-router

Updated on January 26, 2021
author

Joshua Bemenderfer

How To Update Page Title and Metadata with Vue.js and vue-router

Introduction

vue-router is an excellent routing solution for Vue.js, but requires additional configuration to update the page title and metadata on route change. There will be times where you will want the title of the browser to change when the page changes. And for SEO (search engine optimization), you will not want every search result or link to your website to say “Home Page” for all routes.

In this article, you’ll learn how to add this feature yourself. You will build an example Vue application with customizable page title and metadata on route change.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v14.6.0, npm v6.14.7, Vue.js v2.6.11, vue-router v3.2.0, and @vue/cli v4.4.6.

Step 1 — Creating a Vue Project and Installing Dependencies

Let’s create a fresh Vue project.

First, open your terminal and use vue-cli create a Vue project:

  1. npx @vue/cli@4.4.6 create --inlinePreset='{ "useConfigFiles": false, "plugins": { "@vue/cli-plugin-babel": {}, "@vue/cli-plugin-eslint": { "config": "base", "lintOn": ["save"] } }, "router": true, "routerHistoryMode": true }' vue-router-meta-example

This long command is a set of presets based on defaults established by @vue/cli/packages/@vue/cli/lib/options.js. When reformatted for readability, it looks look like this:

{
  "useConfigFiles": false,
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "base",
      "lintOn": ["save"]
    }
  },
  "router": true,
  "routerHistoryMode": true
}

These presets add vue-router as a plugin (cli-plugin-router), enable history mode, add Babel, and add ESLint.

For the needs of this tutorial, you will not require TypesScript, Progressive Web App (PWA) support, Vuex, CSS pre-processors, unit testing, or end-to-end (E2E) testing.

Next, navigate to the new project directory:

  1. cd vue-router-meta-example

At this point, we have a fresh Vue Project to build upon. The next step will be defining sample routes in the application. Once we have established the structure of our application, we will be able to see title and meta changes in action.

Step 2 — Defining Sample Routes and Templates

In our example, our goal will be to construct an application consisting of:

  • a home route (/)
  • an adjacent About route (/about)
  • and a nested Frequently Asked Questions route (/about/frequently-asked-questions)

Now, open main.js:

  1. nano src/main.js

Take a moment to familiarize yourself with how VueRouter has been added by cli-plugin-router:

src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

Now, open router/index.js:

  1. nano src/router/index.js

Take a moment to familiarize yourself with the routes for "Home" and "About" generated by cli-plugin-router. And add the route for the nested "Frequently Asked Questions":

src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import FrequentlyAskedQuestions from '../views/FrequentlyAskedQuestions.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
    children: [
      {
        path: 'frequently-asked-questions',
        component: FrequentlyAskedQuestions,
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

This establishes our desired routing for this tutorial. Notice that we are referencing a view that does not exist yet. We will address that next.

Create a new file called FrequentlyAskedQuestions.vue in the views directory:

  1. nano src/views/FrequentlyAskedQuestions.vue

Then, add the template:

src/views/FrequentlyAskedQuestions.vue
<template>
  <div>
    <h2>Frequently Asked Questions</h2>
    <dl>
      <dt>What is your favorite aquatic animal?</dt>
      <dd>Sharks.</dd>
      <dt>What is your second favorite aquatic animal?</dt>
      <dd>Dolphins.</dd>
      <dt>What is your third favorite aquatic animal?</dt>
      <dd>Cuttlefish.</dd>
    </dl> 
 </div>
</template>

<style>
dt {
  font-weight: bold;
}

dd {
  margin: 0;
}
</style>

We have our new view, but we still need to reference it in the application.

Now, open About.vue:

  1. nano src/views/About.vue

Next, add <router-view> so nested routes display children;

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <router-view/>
  </div>
</template>

Now, open App.vue:

  1. nano src/App.vue

Take a moment to familiarize yourself with how the file is modified by cli-plugin-router. And add <router-link> for "Frequently Asked Questions":

src/App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/about/frequently-asked-questions">FAQ</router-link>
    </div>
    <router-view/>
  </div>
</template>

At this point, we have a Vue application with routes to "Home", "About", and "Frequently Asked Questions". We can run the following command:

  1. npm run serve

And visit localhost:8080 in a web browser. Clicking on the navigation links should display the expected components. However, the <title> and <meta> tags are not changing yet.

Step 3 — Adding Route Meta Fields and a Navigation Guard

vue-router supports Route Meta Fields for title and meta values. Let’s revisit our routes and add meta fields.

Open router/index.js:

  1. nano src/router/index.js

And add meta fields for "Home", "About", and "Frequently Asked Questions":

src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import FrequentlyAskedQuestions from '../views/FrequentlyAskedQuestions.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: {
      title: 'Home Page - Example App',
      metaTags: [
        {
          name: 'description',
          content: 'The home page of our example app.'
        },
        {
          property: 'og:description',
          content: 'The home page of our example app.'
        }
      ]
    }
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
    meta: {
      title: 'About Page - Example App',
      metaTags: [
        {
          name: 'description',
          content: 'The about page of our example app.'
        },
        {
          property: 'og:description',
          content: 'The about page of our example app.'
        }
      ]
    },
    children: [
      {
        path: 'frequently-asked-questions',
        component: FrequentlyAskedQuestions,
        meta: {
          title: 'Nested - About Page - Example App'
        }
      }
    ]
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

However, this will not result in updating the page title and metadata on route change.

To accomplish that, we will need a custom navigation guard.

In the route/index.js file, add a global navigation guard after the routes but before we export router:

src/route/index.js
// ... 

// This callback runs before every route change, including on page load.
router.beforeEach((to, from, next) => {
  // This goes through the matched routes from last to first, finding the closest route with a title.
  // e.g., if we have `/some/deep/nested/route` and `/some`, `/deep`, and `/nested` have titles,
  // `/nested`'s will be chosen.
  const nearestWithTitle = to.matched.slice().reverse().find(r => r.meta && r.meta.title);

  // Find the nearest route element with meta tags.
  const nearestWithMeta = to.matched.slice().reverse().find(r => r.meta && r.meta.metaTags);

  const previousNearestWithMeta = from.matched.slice().reverse().find(r => r.meta && r.meta.metaTags);

  // If a route with a title was found, set the document (page) title to that value.
  if(nearestWithTitle) {
    document.title = nearestWithTitle.meta.title;
  } else if(previousNearestWithMeta) {
    document.title = previousNearestWithMeta.meta.title;
  }

  // Remove any stale meta tags from the document using the key attribute we set below.
  Array.from(document.querySelectorAll('[data-vue-router-controlled]')).map(el => el.parentNode.removeChild(el));

  // Skip rendering meta tags if there are none.
  if(!nearestWithMeta) return next();

  // Turn the meta tag definitions into actual elements in the head.
  nearestWithMeta.meta.metaTags.map(tagDef => {
    const tag = document.createElement('meta');

    Object.keys(tagDef).forEach(key => {
      tag.setAttribute(key, tagDef[key]);
    });

    // We use this to track which meta tags we create so we don't interfere with other ones.
    tag.setAttribute('data-vue-router-controlled', '');

    return tag;
  })
  // Add the meta tags to the document head.
  .forEach(tag => document.head.appendChild(tag));

  next();
});

// ...

At this point, we have a Vue application with routes, meta fields, and navigation guard. We can run the following command:

  1. npm run serve

And visit localhost:8080 in a web browser. Now, when your routes change, the page <title> will be updated with the closest-matched route’s title. Likewise, the <meta> tags will update as well.

Conclusion

In this tutorial, you learned how to use meta fields and navigation guards to update the page title and metadata on route change.

If you use prerendering, then these changes will be baked into your prerendered HTML files and will work great for SEO. For SSR (server-side rendering), it can be a bit more complicated.

It’s also worth noting that dynamic, frequently-updating titles are out of the question with this method. You’ll probably have to stick with manually updating document.title for such use cases.

If you’d like to learn more about Vue.js, check out our Vue.js topic page for exercises and programming projects.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Joshua Bemenderfer

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
4 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Nice work, thanks! You helped me)

You frogot about const previousNearestWithMeta in example

// If a route with a title was found, set the document (page) title to that value.
  if (nearestWithTitle) {
    document.title = nearestWithTitle.meta.title
  } else { // ← here ↓
    document.title = previousNearestWithMeta.meta.title
  }

or we get an error ‘previousNearestWithMeta’ is assigned a value but never used no-unused-vars if use eslint

I just LOVE your tutorials. To the point and excellent error coverage. Thanks a lot. long time fan here :)

This worked out perfectly. Thank you!!!

Also, I think the line

const router = new VueRouter({
  routes,
  mode: 'history'
});

Need to be before

router.beforeEach((to, from, next) => {

Right?

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.