vuejs / vue-hackernews-2.0

HackerNews clone built with Vue 2.0, vue-router & vuex, with server-side rendering
MIT License
10.96k stars 2.15k forks source link

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

Open valentinvieriu opened 7 years ago

valentinvieriu commented 7 years ago

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

ktquez commented 7 years ago

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

jazoom commented 7 years ago

Can confirm, vue-meta seems to work well.

valentinvieriu commented 7 years ago

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?

LinusBorg commented 7 years ago

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.

jazoom commented 7 years ago

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

LinusBorg commented 7 years ago

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.

jazoom commented 7 years ago

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.

LinusBorg commented 7 years ago

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

lijiakof commented 7 years ago

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!

jazoom commented 7 years ago

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.

ram-you commented 7 years ago

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 ?

daliborgogic commented 7 years ago

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

ram-you commented 7 years ago

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.

daliborgogic commented 7 years ago

@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

ram-you commented 7 years ago

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

daliborgogic commented 7 years ago

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

screenshot_20170216_134709

sutra commented 7 years ago

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

jqEmprendedorVE commented 6 years ago

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 commented 6 years ago

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

desicne commented 6 years ago

@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?

Eric-Bryan commented 6 years ago

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 :

`

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

sutra commented 6 years ago

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

Lord-Y commented 6 years ago

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

b02505048 commented 5 years ago

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

aj-amelio311 commented 5 years ago

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

aj-amelio311 commented 5 years ago

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.