vue-hackernews-2.0 icon indicating copy to clipboard operation
vue-hackernews-2.0 copied to clipboard

How to modify section in the <head/> like title and meta info

Open valentinvieriu opened this issue 8 years ago • 26 comments

Let's say that this needs to be SEO friendly. What would be the best practice of making the

section editable but keep also the streaming approach

valentinvieriu avatar Nov 14 '16 12:11 valentinvieriu

@valentinvieriu I using vue-meta https://github.com/declandewet/vue-meta

ktquez avatar Nov 14 '16 19:11 ktquez

Can confirm, vue-meta seems to work well.

jazoom avatar Nov 17 '16 00:11 jazoom

vue-meta looks lt can do the work. As Vue-hacker-news2.0 should be the main example on how to build an isomorphic app, shouldn't we have this use case built in? Should we try to incorporate vue-meta in the vue-hackernews-2.0?

valentinvieriu avatar Nov 17 '16 09:11 valentinvieriu

Though vue-meta looks like a very fine plugin, I don't think we should rely on 3rd-party Vue extensions to in an official core feature demo-app. This would make it seem that

  1. it's official(-ly supported), which it isn't and
  2. it's "nessessary"

Especially since its README says:

Please note that this project is still in very early alpha development and is not considered to be production ready.

LinusBorg avatar Nov 17 '16 10:11 LinusBorg

I'd say it's about as necessary as a server-side-rendered app, which is officially supported.

jazoom avatar Nov 17 '16 10:11 jazoom

I'd say it's about as necessary as a server-side-rendered app, which is officially supported.

Nessessary enough that we should include a not-production-ready 3rd-party plugin into an official demo that is supposed to demonstrate "best practice"? I think that would be a bad idea.

LinusBorg avatar Nov 17 '16 11:11 LinusBorg

I agree. I wasn't suggesting that. If it's as necessary as SSR, which many might believe it is, perhaps there should be an official way of doing it?

FYI: I'm happy just using a library like this one.

jazoom avatar Nov 17 '16 11:11 jazoom

If it's as necessary as SSR, which many might believe it is, perhaps there should be an official way of doing it?

Good topic for a feature request on the repo @ www.github.comvuejs/vue/issues

LinusBorg avatar Nov 17 '16 11:11 LinusBorg

vue-meta this module has some problem on Vue SSR‘s performance. I use 0.4.4 version, and it let memory more and more big, and affect rps. Use it carefully!

lijiakof avatar Nov 29 '16 05:11 lijiakof

I've also come across a few bugs with it that I'm having a hard time working around. It would be good if there was an official library that worked so we don't have everyone reinventing their own libraries or hacks around existing ones.

jazoom avatar Nov 29 '16 08:11 jazoom

As this repository is for vue ssr best practice, I ask @yyx990803 and @addyosmani (because they are the first and second contributor to this project) what they suggest about it ?

ram-you avatar Dec 03 '16 14:12 ram-you

It's easy to modify meta tags. Proof of concept: Live Demo Repo

daliborgogic avatar Feb 14 '17 19:02 daliborgogic

Hi @daliborgogic Can you give us more explanation how does it work. Because I understood the change of document title fired by App.vue but I cant make the changes on og:image and og.url . Thank you in advance.

ram-you avatar Feb 16 '17 09:02 ram-you

@ram-you For example in HomeView we have function to set title, description. image, etc in store. On preFetch store will be updated with this data. When serving head we replace marker with data from context #L74

You can test: Example on: https://developers.facebook.com/tools/debug/sharing/ https://cards-dev.twitter.com/validator

daliborgogic avatar Feb 16 '17 11:02 daliborgogic

@daliborgogic It doesn't work. If you take, for example the link https://vuejs-ssr-meta-tmflbxpghm.now.sh/contact in Facebook Debug you will see that in <meta property="og:title" content="Home" /> the content still "Home" and not "Contact" . Furthermore, the only change is done in App.vue for title is on client rendering not in SSR.

ram-you avatar Feb 16 '17 12:02 ram-you

@ram-you og:url fix and Updated example Please use issues

screenshot_20170216_134709

daliborgogic avatar Feb 16 '17 12:02 daliborgogic

I am using vue-head, even use it to load css file to implement themeable pages.

sutra avatar Sep 06 '17 03:09 sutra

Hi everyone,

I'm develop some of code for manage the head, but i have some problem. I take the example from hackernew and ssr documentation, and i create a head.js file this looks like this:

const cleanMetas = () => {
  /*
  TODO:
  delete all metas
  add metas elementary
  return to recreate head from vm.$options
   */
}

const getString = (vm, content) => {
  return typeof content === 'function'
    ? content.call(vm)
    : content
}

export const getMeta = (vm, meta, env) => {
  if(typeof meta !== 'object')
    return

  if(env){
    return Object.keys(meta)
    .map(value => {
      return Object.keys(meta[value])
        .map(key => `${key}="${getString(vm, meta[value][key])}"`)
        .join(" ");
    })
    .map(value => `  <meta ${value} >`)
    .join("\n");

  } else {
    return meta
  }
}

const serverHeadMixin = {
  created () {
    const { head } = this.$options
    if(head){
      const { title } = head 
      if(title){
        this.$ssrContext.title = `${getString(this, title)} :: Vue SSR`
      }

      const { meta } = head 
      if(meta)
        this.$ssrContext.meta = `\n${getMeta(this, meta, true)}`
    }
  }
}

const clientHeadMixin = {
  mounted () {
    cleanMetas()

    const { head } = this.$options
    if(head){
      const { title } = head 
      if(title){
        document.title = `${getString(this, title)} :: Vue SSR`
      }

      const { meta } = head 
      /*
     TODO:
     recreate new metas set in this.$options.head.meta
      */

    }
  }
}

export default process.env.VUE_ENV === 'server'
  ? serverHeadMixin
  : clientHeadMixin

and i add like a mixin in app.js like this:

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'
import Firebase from './firebase/plugin'
import headMixin from './util/head'

Vue.mixin(headMixin)

export function createApp (context) {
  const router = createRouter()
  const store = createStore()

  /*
   * Add property $firebase tu Vue instance
   */
  Vue.use(Firebase, store.state.config)

  // sync so that route state is available as part of the store
  sync(store, router)

  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  return { app, router, store }
}

One of my component looks like that:

<template>
  <div class="home">
    <h3>Sección del Home {{ data }}</h3>
    <p>Area de trabajo inicial</p>
    <pre>{{ item }}</pre>
  </div>
</template>
<script>
  export default {
    head: {
      title() {return `${this.name}  con `} ,
      meta: [
        { name: 'description', content() { return `El perfil de ${this.name}`} },
        { name: 'keywords', content() { return `${this.name} ${this.lastname} ${this.email}` } },
        { name:"twitter:card", content:"summary"},
        { name:"twitter:site", content:"@jqEmprendedorVE"},
        { name:"twitter:creator", content:"@jqEmprendedorVE"},
        { name:"twitter:url", content:"https://vue-firebase-ssr.firebaseapp.com"},
        { name:"twitter:title", content() {return `${this.name} creando Vuejs SSR + Firebase`}},
        { name:"twitter:description", content:"Modelo de Vuejs SSR con Firebase Cloud Function + Hosting"},
        { name:"twitter:image", content:"https://www.filepicker.io/api/file/nS7a8itSTcaAsyct6rVp"}
      ]
    },
    asyncData ({ store, route }) {
      // return the Promise from the action
      return store.dispatch('fetchItem', 1)
    },
    computed: {
      // display the item from store state.
      item () {
        return this.$store.state.items[1]
      },
      name () {
        return this.$store.state.items[1].nombre
      },
      lastname () {
        return this.$store.state.items[1].apellido
      },
      email () {
        return this.$store.state.items[1].correo
      }
    },
    data() {
      return {
        data: ':: SSR'
      }
    },
    created() {
      this.$firebase.db().ref('data').once('value', snapshot=>{
        // console.log(snapshot.val())
      })
    }
  }
</script>

My problem is the following, when rendering on the client side, and changing the view to the other, the new route maintains the metatag of the previous view when doing CSR, I have no problem to load on the SSR, but on the client It does not look complete yet.

Up to now I can load dynamics:

title, CSR and SSR meta only SSR

DEMO Repo

jqEmprendedorVE avatar Dec 24 '17 23:12 jqEmprendedorVE

Ready,

I finished...

  1. I create a head.js for a Mixin
const cleanMetas = () => {
  return new Promise ((resolve, reject)=>{
    const items = document.head.querySelectorAll('meta')
    for(const i in items) {
      if(typeof items[i]==='object' && ['viewport'].findIndex(val=>val===items[i].name)!=0 && items[i].name!=='')
        document.head.removeChild(items[i])
    }
    resolve()
  })
}

const createMeta = (vm, name, ...attr) => {
  const meta = document.createElement('meta')
  meta.setAttribute(name[0], name[1])
  for(const i in attr){
    const at = attr[i]
    for(const k in at) {
      meta.setAttribute(at[k][0], getString(vm, at[k][1]))
    }
  }
  document.head.appendChild(meta);
}

const getString = (vm, content) => {
  return typeof content === 'function'
    ? content.call(vm)
    : content
}

export const getMeta = (vm, meta, env) => {
  if(typeof meta !== 'object')
    return

  if(env){
    return Object.keys(meta)
    .map(value => {
      return Object.keys(meta[value])
        .map(key => `${key}="${getString(vm, meta[value][key])}"`)
        .join(" ");
    })
    .map(value => `  <meta ${value} >`)
    .join("\n");

  } else {
    return meta
  }
}

const serverHeadMixin = {
  created () {
    const { head } = this.$options
    if(head){
      const { title } = head 
      if(title){
        this.$ssrContext.title = `${getString(this, title)} :: Vue SSR`
      }

      const { meta } = head 
      if(meta)
        this.$ssrContext.meta = `\n${getMeta(this, meta, true)}`
    }
  }
}

const clientHeadMixin = {
  mounted () {
    const vm = this

    const { head } = this.$options
    if(head){
      const { title } = head 
      if(title){
        document.title = `${getString(this, title)} :: Vue SSR`
      }

      cleanMetas().then(()=>{
        const { meta } = head 
        if(meta){
          for(const nm in meta) {
            const name = Object.entries(meta[nm])[0]
            const attr = Object.entries(meta[nm]).splice(1,Object.entries(meta[nm]).length)
            createMeta(vm, name, attr)
          }
        }
      })
    }
  }
}

export default process.env.VUE_ENV === 'server'
  ? serverHeadMixin
  : clientHeadMixin

  1. Include in app.js
...
import headMixin from './util/head'

Vue.mixin(headMixin)
...

  1. Set in your component
<template>
  <div class="home">
    <h3>Sección del Home {{ data }}</h3>
    <p>Area de trabajo inicial</p>
    <pre>{{ item }}</pre>
  </div>
</template>
<script>
  export default {
    head: {
      title() {return `${this.name}  con `} ,
      meta: [
        { name: 'description', content() { return `El perfil de ${this.name}`} },
      ]
    },
    asyncData ({ store, route }) {
      // return the Promise from the action
      return store.dispatch('fetchItem', 1)
    },
    computed: {
      // display the item from store state.
      item () {
        return this.$store.state.items[1]
      },
      name () {
        return this.$store.state.items[1].nombre
      },
      lastname () {
        return this.$store.state.items[1].apellido
      },
      email () {
        return this.$store.state.items[1].correo
      }
    },
    data() {
      return {
        data: ':: SSR'
      }
    },
    created() {
      this.$firebase.db().ref('data').once('value', snapshot=>{
        // console.log(snapshot.val())
      })
    }
  }
</script>

The content data can be string or a function, combine function with computed and asynData for a one best experience in pre-fetching

jqEmprendedorVE avatar Dec 25 '17 00:12 jqEmprendedorVE

@jqEmprendedorVE I tried your approach and it does append the meta's in the head, (I see it in inspector) but once I try to share something it does not seem to work. Like there is no meta tags there. Also site inspectors like (https://opengraphcheck.com/) do not seem to pick it up as well

Any ideas why? And how to fix it?

desicne avatar May 07 '18 16:05 desicne

Hello @sutra, I know it's an old question. But can you send me a little example of vue-head code to set css file dynamically, please ? I tried to use vue-head but all I get is the

from index.html (set in my app).

Here is my App.vue where I use vue-head :

`

<input ref="input1" type="text" size="100"/><button @click="testMethod()">Rendu html</button>
<br/>
<span v-html="htmlText"></span>

<span class="titleRed">It's red</span>
</div>

` This vue fails when app is loaded and I can see in source that the head comes from index.html. What am I doing wrong ?

Here is App.vue :

[

](url)

Thanks a lot in advance.

Eric-Bryan avatar Jun 07 '18 08:06 Eric-Bryan

@Eric-Bryan on Sep 27, 2017, I have replaced vue-head with vue-meta in my project.

sutra avatar Jun 24 '18 15:06 sutra

@desicne You have to fulfill your code with og tags:

head: {
	title() {
		return "Hello world"
	},
	meta: [
		{ name: "description", content: "summary" },
		{ name: "keywords", content: "summary" },
		{ name: "twitter:card", content: "summary" },
		{ name: "twitter:site", content: "@jqEmprendedorVE" },
		{ name: "twitter:creator", content: "@jqEmprendedorVE" },
		{ name: "twitter:url", content: "https://vue-firebase-ssr.firebaseapp.com" },
		{ name: "twitter:description", content: "Modelo de Vuejs SSR con Firebase Cloud Function + Hosting" },
		{ name: "twitter:image", content: "https://www.filepicker.io/api/file/nS7a8itSTcaAsyct6rVp" },
		{ property: "og:title", content: "summary" },
		{ property: "og:type", content: "summary" },
		{ property: "og:url", content: "summary" },
		{ property: "og:image", content: "summary" },
		{ property: "twitter:card", content: "summary" }
	]
}

Working for me.

Lord-Y avatar Aug 05 '18 17:08 Lord-Y

Hi @jqEmprendedorVE , your code works well! Thanks! But one question, the added meta will not remove itself, any idea?

b02505048 avatar Oct 29 '18 11:10 b02505048

all this trouble just to add some meta tags...

aj-amelio311 avatar Mar 27 '19 21:03 aj-amelio311

plain javascript worked for me. in the index.html file, at the bottom of the body:

<script>
document.title = "whatever";
var metaTag=document.createElement('meta');
metaTag.name = "viewport"
metaTag.content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
document.getElementsByTagName('head')[0].appendChild(metaTag);
</script>

this is definitely something the developers of Vue need to improve in future updates.

aj-amelio311 avatar Mar 30 '19 23:03 aj-amelio311