108

I would like to specify my titles within the route definition if possible. What is normally specified in <head><title> and appears in the browser title bar.

I have my project set up as follows:

main.js

import Vue from 'vue'
import App from './App.vue'
import VeeValidate from 'vee-validate';
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(VeeValidate);
Vue.use(ElementUI);
Vue.config.productionTip = false

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

router.js

import Vue from 'vue'
import Router from 'vue-router'
import Skills from './components/Skills.vue'
import About from './components/About.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'skills',
      component: Skills,
      meta: { title: 'Skills - MyApp' } // <- I would to use this one
    },
    {
      path: '/about/:name',  // Add /:name here
      name: 'about',
      component: About,
      meta: { title: 'About - MyApp' }
    }
  ]
})

Preferably, I would want an automatic system instead of changing page title on the created function of every component. Thanks.

ghybs
  • 47,565
  • 6
  • 74
  • 99
darkhorse
  • 8,192
  • 21
  • 72
  • 148

16 Answers16

162

You can use a navigation guard with the router definition:

import Vue from 'vue';

const DEFAULT_TITLE = 'Some Default Title';
router.afterEach((to, from) => {
    // Use next tick to handle router history correctly
    // see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
    Vue.nextTick(() => {
        document.title = to.meta.title || DEFAULT_TITLE;
    });
});

You'll need to change your export to:

const router = new Router({ ... });
...
export default router;

Or you can use an immediate watcher on your root component:

export default {
    name: 'App',
    watch: {
        $route: {
            immediate: true,
            handler(to, from) {
                document.title = to.meta.title || 'Some Default Title';
            }
        },
    }
};
Steven B.
  • 8,962
  • 3
  • 24
  • 45
  • 1
    Wouldn't it be better to use `beforeEach`? – adadion Feb 01 '19 at 04:56
  • 1
    Ah, got it! Nice info. I'm using `beforeEach`, so I have to call `next()` just like in the [doc](https://router.vuejs.org/guide/advanced/meta.html). – adadion Feb 01 '19 at 06:19
  • 7
    I think I would argue that `afterEach` is better practice in this case, since it isn't something that has to happen before the navigation can occur (e.g. authentication checks go in `beforeEach` since they have the potential to cancel the navigation, update visual stuff in `afterEach` when the navigation has already been confirmed), but I don't think it makes any difference in terms of performance or anything if it's the only thing you need to do. – eritbh Mar 07 '19 at 21:37
  • Nice, but how do I set the title initially (when the page is opened manually via url in address bar, not via navigating)? (for now I have only tested the `watch` approach and it doesn't set title in that case which is kinda predictable) – YakovL Nov 05 '19 at 17:16
  • for error safety, I'd also use `to.meta && to.meta.title || DEFAULT_TITLE;` so that if `meta` is not defined, `DEFAULT_TITLE` is returned instead of throwing an exception (unless that is written with TypeScript and routes are required to have `meta`) – YakovL Nov 05 '19 at 17:20
  • Ah, ok, I've found a solution, posted as an answer – YakovL Nov 05 '19 at 17:33
  • @YakovL You don't need to check that `.meta` exists because it always will. The object you supply is just spread into the existing object. If you are using the navigation guard, `afterEach`, direct navigation will still hit that guard which is why I would suggest using the guard over a watcher. – Steven B. Nov 05 '19 at 17:35
  • @StevenB. thanks for clarifying! May be some of this deserves a spot in the answer itself – YakovL Nov 05 '19 at 18:07
  • 1
    If you use a watcher, make sure to specify `immediate: true` so that it works on initial page load. `watch: { $route: { handler(to, from) {...}, immediate: true, }` – jwkicklighter Dec 18 '19 at 19:45
  • @StevenB. Did you see the github page referenced in carl-johan's response (below)? This seems critical to preserving the correct history. – Cato Minor Feb 08 '20 at 21:38
  • 2
    @CatoMinor I updated my answer. From testing, the watcher method appears to work fine without `nextTick`. – Steven B. Feb 09 '20 at 02:12
  • The `immediate watcher` method is what worked for me, thanks a lot! – Patrick Feb 25 '22 at 05:50
  • Just wanted to add my 2 cents after some research (i'm not sure how current this info is). Depending on how long it takes for your route to load and whether or not it blocks, this could be a material difference in if that content sent in `afterEach` is actually picked up by google bot or other crawlers. More thoughts found in this article: https://www.smashingmagazine.com/2019/05/vue-js-seo-reactive-websites-search-engines-bots/#the-results – Eric Uldall Mar 03 '22 at 08:02
17

Latest Works way in 2021- Vue3:

Add the line name for related Component in .\router\index.js

  {
  path: '/',
  name: 'Home page'
  },

Load it in BeforeEach this function also write it in .\router\index.js

router.beforeEach((to, from, next) => {
  document.title = to.name;
  next();
});
W Kenny
  • 1,855
  • 22
  • 33
16

Advanced variant

Using vue-meta

first run npm install vue-meta

and include it into your main.js;

import VueMeta from 'vue-meta'
Vue.use(VueMeta)

after doing so you can add a metaInfo() method to every vue component, handling meta data;

metaInfo() {
        return { 
            title: "Epiloge - Build your network in your field of interest",
            meta: [
                { name: 'description', content:  'Epiloge is about connecting in your field of interest. Our vision is to help people share their knowledge, work, projects, papers and ideas and build their network through what they do rather where they live, study or work.'},
                { property: 'og:title', content: "Epiloge - Build your network in your field of interest"},
                { property: 'og:site_name', content: 'Epiloge'},
                {property: 'og:type', content: 'website'},    
                {name: 'robots', content: 'index,follow'} 
            ]
        }
    }

Furthermore this can be used for dynamic meta info;

export default{
    name: 'SingleUser',
    data(){
        return{
            userData: {},
            ...
            aws_url: process.env.AWS_URL,
        }
    },  
    metaInfo() {
        return {
            title: `${this.userData.name} - Epiloge`,
            meta: [
                { name: 'description', content: 'Connect and follow ' + this.userData.name + ' on Epiloge - ' + this.userData.tagline},
                { property: 'og:title', content: this.userData.name + ' - Epiloge'},
                { property: 'og:site_name', content: 'Epiloge'},
                { property: 'og:description', content: 'Connect and follow ' + this.userData.name + ' on Epiloge - ' + this.userData.tagline},
                {property: 'og:type', content: 'profile'},
                {property: 'og:url', content: 'https://epiloge.com/@' + this.userData.username},
                {property: 'og:image', content: this.aws_url + '/users/' + this.userData.profileurl + '-main.jpg' }    
            ]
        }
    },
    ...
}

Source: Medium - How to add dynamic meta-tags to your Vue.js app for Google SEO

nonNumericalFloat
  • 1,348
  • 2
  • 15
  • 32
11

I'd like to add that above doesn't really preserve history as it should. See https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609 for a better answer that actually takes care of the history (albeit a little bit hacky).

8

Actually, based on my experiments with Steven B.'s solution, I've came up with something a bit better. The thing is this

watch: {
    $route(to, from) {
        document.title = to.meta.title || 'Some Default Title';
    },
}

doesn't work when we visit the page initially (by navigating via brower's address bar). Instead, we can create a getter like this:

computed: {
    pageTitle: function() {
        return this.$route.meta.title;
    }
}

Now in my case I was looking to set the "template"'s header (so that children routes don't bother about it) so that was it; for your case you may wonder how to set document's title once you have computed property, and there are some ways. Based on those answers, you can even do:

created () {
    document.title = this.$route.meta.title;
}

but I'd test this for the case of revisiting the same page (not sure if the component is created each time) before using in production.

YakovL
  • 7,557
  • 12
  • 62
  • 102
6

With Vue 3 i am ended with this solution:

const routes = [  
  {
    path: '/calendar',
    name: 'calendar',
    meta: { title: 'My Calendar' },
    component: CalendarForm
  },
  {
    path: '/',
    name: 'main',
    meta: { title: 'Home page' },
    component: MainForm
  }  
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

router.beforeEach((to, from, next) => {
  console.log(to);
  document.title = to.meta.title;
  next();
});

export default router
Vasilij Altunin
  • 754
  • 1
  • 5
  • 18
  • This is much better than some "Helmet" component you have to 'npm install' in my opinion. And it keeps the solution in one place (router.js) if you're lucky. – jaletechs Apr 16 '23 at 16:37
3

I found this solution which is using mixins and needs minimal code.

https://medium.com/@Taha_Shashtari/the-easy-way-to-change-page-title-in-vue-6caf05006863 and originally https://github.com/vuejs/vue-hackernews-2.0/blob/master/src/util/title.js

I like it because you can define your title in your view components instead of the routes:

In src/mixins directory create a new file called titleMixin.js with the content below. It is checking if the value of the 'title' property of the component is a variable or function and returns the value of the title variable or return value of the title() function.

function getTitle (vm) {
  const { title } = vm.$options
  if (title) {
    return typeof title === 'function'
      ? title.call(vm)
      : title
  }
}export default {
  created () {
    const title = getTitle(this)
    if (title) {
      document.title = title
    }
  }
}

Register the mixin globally in your main.js. Add this before you create the Vue instance:

import titleMixin from './mixins/titleMixin'

Vue.mixin(titleMixin)

Last, in the view component file ( e.g Home.vue ) use a property named title to define the title of the page.

export default {
  name: 'Home',
  title: 'Homepage Title',
  components: {
    ...
  }
}

A little flaw: The mixin is registered globally, which makes the mixin available for all components , even for non view components where it makes no sense.

Michael S
  • 738
  • 1
  • 8
  • 20
2

For vue 3:

Navigation Guards

import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(config.publicPath),
  routes,
});

const DEFAULT_TITLE = 'Some Default Title';
router.beforeEach((to) => {
  document.title = to.meta.title || DEFAULT_TITLE;
});

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 07 '21 at 09:43
  • 1
    Its better to replace afterEach with beforeEach so you don't see old title before page reloads completely – Mohammad Ghonchesefidi Dec 13 '21 at 20:51
2

Similar to answer by @Steven.B and taken from > https://github.com/vuejs/vue-router/issues/914#issuecomment-1019253370

add this to main.js (Vue.js V3)

import {nextTick} from 'vue';

const DEFAULT_TITLE = "Default Title";
router.afterEach((to) => {
    nextTick(() => {
        document.title = to.meta.title || DEFAULT_TITLE;
    });
});
Chris
  • 1,720
  • 16
  • 35
1

Oh look another package

I see lots of great solutions here and at risk of beating a dead horse I submit: https://www.npmjs.com/package/@aminoeditor/vue-router-seo

It's <1mb, has no dependencies and has flexible implementations all easily controlled in your route config. It also has async support for title and other meta data.

Basic example

import { seoGuardWithNext } from '@aminoeditor/vue-router-seo';
const routes = [{
    path: '/',
    component: Home,
    meta: {
        seo: {
            title: 'My title here',
            metaTags: [
                {
                    name: 'description',
                    content: 'My Description here'
                },
                {
                    name: 'keywords',
                    content: 'some,keywords,here'
                }
            ],
            richSnippet: {
                "@context": "https://schema.org",
                "@type": "Project",
                "name": "My Project",
                "url": "https://exampl.com",
                "logo": "https://example.com/images/logo.png",
                "sameAs": [
                    "https://twitter.com/example",
                    "https://github.com/example"
                ]
            }
        }
    }
},{
    path: '/about',
    component: About,
    meta: {
        seo: {
            // do some async stuff for my page title
            title: async route => {
                const data = await fetch('somedataurl');
                return `${data} in my title!`;
            }
        }
    }
}]

const router = VueRouter.createRouter({
    history: VueRouter.createWebHashHistory(),
    routes,
})

// install the seo route guard here
router.beforeEach(seoGuardWithNext)

const app = Vue.createApp({})
app.use(router)
app.mount('#app')
Eric Uldall
  • 2,282
  • 1
  • 17
  • 30
1

As the page title often repeats some text content from the view (e.g. header on the page), I suggest to use a directive for this task.

const updatePageTitle = function (title) {
    document.title = title + ' - My Cool Website';
};

Vue.directive('title', {
    inserted: (el, binding) => updatePageTitle(binding.value || el.innerText),
    update: (el, binding) => updatePageTitle(binding.value || el.innerText),
    componentUpdated: (el, binding) => updatePageTitle(binding.value || el.innerText),
});

On every view you can decide which element content will be treated as the browser title:

<h1 v-title>Some static and {{ dynamic }} title</h1>

Or you can build the title inside the directive value:

<div v-title="'Some static and ' + dynamic + 'title'">...</div>
fracz
  • 20,536
  • 18
  • 103
  • 149
1

for them how use vitness vue 3 template with typescript and ue Pages plugin for dynamically create routes from files, you can done it like this:

import { nextTick } from 'vue'

export const createApp = ViteSSG(
  App,
  { routes, base: import.meta.env.BASE_URL },
  (ctx) => {
    // ...

    ctx.router.afterEach((to) => {
      
        let title = 'AppTitle'
        if (to.meta.title)
          title = `${to.meta.title} - ${title}`
      nextTick(() => {
        document.title = title
      })
    })
  },
)

on each page component you can define specific title like this:

<route lang="yaml">
meta:
  title: PageTitle
</route>

Saeid Doroudi
  • 935
  • 1
  • 9
  • 25
1

For composition API:

Create a new file i.e DefineMeta.ts

import type { Ref } from "vue";

interface MetaArgs
{
    title?: Ref<string> | string;
}

export const defineMeta = (() =>
{
    const title = ref("");
    const metaStack = [] as MetaArgs[];

    watch(title, () =>
    {
        document.title = unref(title);
    });

    return function (args: MetaArgs)
    {
        if (!getCurrentScope())
            throw new Error("defineMeta must be called within a setup function.");

        // Keep track of the last meta added to the stack, as the last meta in the stack is the current meta.
        metaStack.push(args);

        // When the component is unloaded, remove its meta args from the stack
        onUnmounted(() =>
        {
            metaStack.splice(metaStack.indexOf(args), 1);

            // Set the title to the last meta in the stack
            title.value = unref(metaStack.at(-1)?.title) ?? "";
        });

        // Watch for when the title changes and update the document's title accordingly
        if (args.title)
        {
            watch(ref(args.title), () =>
            {
                if (!metaStack.includes(args))
                    return;

                title.value = unref(args.title)!;
            }, { immediate: true });
        }

        return {
            title
        };
    };
})();

Then inside your component's setup method:

const { title } = defineMeta({
    title: computed(() => `${unref(formName)} Details`)
});

As this code keeps track of the title in a stack, make sure to define the default title in your entry / root / TheApp component:

defineMeta({
    title: "Stackoverflow"
});
1

In your router file, define a more detailed path with the title as a meta param

    {
        path: '/somepath',
        name: "mypath-name",
        component: MyComponent,
        meta: { title: 'MY DYNAMIC TITLE' },
    },

and using basic JS

document.title = this.$route.meta.title;
PHPer
  • 637
  • 4
  • 19
0

I was looking for editing the title but not necesseraly with a router. I found that with the mounted method it is possible to do it as well.

new Vue({
  mounted: function() {
    document.title = 'WOW VUE TITLE'
  }
})
Jerem
  • 47
  • 4
-2

If your VUE.JS site has an index.html file. You can just do this:

<html lang="en">
     <head>
           <title>YOUR TITLE HERE</title>

In my case, vue 2.x version working with Vuetify. Just went to the 'public' folder and changed the title tag from "<%= htmlWebpackPlugin.options.title %>" to the one I wanted.

jvictorjs
  • 44
  • 5