Tutorial

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

Vue.js

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:

  • 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:

  • 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:

  • 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:

  • 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:

  • 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:

  • 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:

  • 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:

  • 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:

  • 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:

  • 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.

Creative Commons License