nuxt / rfcs

RFCs for changes to Nuxt.js
99 stars 2 forks source link

vue-meta 3.0 #19

Closed atinux closed 3 years ago

atinux commented 5 years ago

Introducing @nuxt/vue-app-head to replace vue-meta.

Issues of vue-meta1.0

vue-meta 2.0

Will be simply a refactor of the actual one with optimisations and bug fixes.

vue-meta 3.0

vue-meta is the default package used by @nuxt/vue-app but it could be disabled by another module (introducing the way to work with hooks with these external modules).

I believe we could introduce a new component: <n-head>

Example (pages/users/[userId].vue):

<template>
  <div>
    <n-head>
      <title>{{ user.name }}</title>
      <meta key="description" name="description" :content="user.description">
    </n-head>
    <pre>{{ user }}</pre>
  </div>
</template>

<script>
export default {
  async asyncData({ $axios, params }) {
    const user = await $axios.$get(`https://.../api/users/${params.userId}`)

    return { user }
  }
}
</script>

The point of this component is simply a shortcut to write inside the head key (we should keep this key to let librairies like Vuetify add mixins for it when used with Nuxt).

Here, key is simply the vmid we have in head, I believe by using a functional component, we could achieve easily this behaviour.

This <n-head> should have some props to handle body-attrs and html-attrs, about head-attrs, well, it's all the others non-defined props directly :)

What do you think?

manniL commented 5 years ago

~~As a vue-meta contributor, I can only agree 👍 It'd be great to have a better solution than relying on vue-meta because of the cumbersome process and the space for improvements we'd have by building our own solution.~~

now that we have control over the repo, let's improve it :muscle:

Have you thought about going for a completely separate block? Like:

<template>
  <div>
    <pre>{{ user }}</pre>
  </div>
</template>

<script>
export default {
  async asyncData({ $axios, params }) {
    const user = await $axios.$get(`https://.../api/users/${params.userId}`)

    return { user }
  }
}
</script>

<n-head>
  <title>{{ user.name }}</title>
  <meta key="description" name="description" :content="user.description">
</n-head>
atinux commented 5 years ago

I was thinking also or using a separate block, and by looking at the example, I prefer it :)

But I don't know how to do something like this, I guess it's a plugin for vue-template-compiler? cc @znck

manniL commented 5 years ago

@Atinux We can probably look at https://github.com/kazupon/vue-i18n-loader for an overview ☺️

znck commented 5 years ago

A webpack loader is required for handling the custom block. See https://vue-loader.vuejs.org/guide/custom-blocks.html#example

P.S. There's no need to add prefix n-.

<head>
  <title>{{ user.name }}</title>
  <meta key="description" name="description" :content="user.description">
</head>

<template>
  <div>
    <pre>{{ user }}</pre>
  </div>
</template>

<script>
export default {
  async asyncData({ $axios, params }) {
    const user = await $axios.$get(`https://.../api/users/${params.userId}`)

    return { user }
  }
}
</script>
atinux commented 5 years ago

Thanks @znck for the clarifications :)

The main issue I see here with <head> block, is to not be able to use Vue syntax (v-if, v-bind, etc). As well as the auto completion from the HTML & Vue components definitions...

pimlie commented 5 years ago

Maybe we can take some features from headful as well? E.g. to have the possibility to add both title, itemprop=name, og:title as twitter:title tags by only setting a title would be nice. I could imagine this to be configurable by enabling microdata, opengraph and/or twitter props in the config.

manniL commented 5 years ago

@pimlie Definitely! We could make meta tag definitions way easier (and also incorporate changes from the meta package of https://github.com/nuxt-community/pwa-module)

pimlie commented 5 years ago

Another feature that would be nice to have, would be if this package provided (the tools for) a specific event for tracking meta changes after vue-router navigation. The problem with vue-meta is that the meta changes are asynchronously implemented after navigation. You can listen for the vue-meta.changed event, but that event doesnt always trigger as it only triggers after some meta changes happened. (Also it seems the name changed in vue-meta is slightly wrong, updated would be more accurate as it also fires if you update with exactly the same meta information).

This would solve a small issue I have with nuxt-matomo where I would like to include the document.title in page tracking.

manniL commented 5 years ago

PPS: https://github.com/declandewet/vue-meta is now https://github.com/nuxt/vue-meta :speak_no_evil:

nathanchase commented 5 years ago

A good resource of things to test against, if someone wanted to implement a test suite for a vue-meta replacement: https://github.com/joshbuchea/HEAD

FWIW, the ease of ability to modify the head and meta tags for SSR is the primary reason I chose Nuxt in the first place as a platform over others.

Timkor commented 5 years ago

I think it would be a clean solution to have a separate component for the head just like the loader component.

<template>
    <head>
        <title>{{head.title}}</title>
    </head>
</template>
<script>
    export default {
        props: {
            head: Object // Old head output of page components
        }
    }
</script>

This requires nearly zero overhead. A default head component can be included like the loading indicator. But the head component can also be customized. It can be made that similar input is accepted like vue-meta did. This way it is pretty compatable.

I do not think it's the page component's responsibility to render the head. It should supply some data at most. It is pretty important information regarding SEO. If a lot of people are working in a project, one (less experienced developer) should not easily be able to adjust head information. That's why I think nesting the n-head in the template of page components is a bad idea. It can also be easily overseen in merge conflicts and reviews.

nathanchase commented 5 years ago
<head>
   <title v-if="user">{{ user.name }}</title>
   <title v-else>{{ page.name }}</title>
   <meta key="description" name="description" :content="user.description" v-if="user">
</head>

<template>
...
</template>

<script>
...
</script>

I like this syntactically more than any other solution.

atinux commented 5 years ago

Thanks for your comments :)

@pimlie already started a refactor of vue-meta: https://github.com/nuxt/vue-meta/tree/next

I believe we don't need @nuxt/vue-app-head but directly introduce breaking changes with vue-meta 2.0 (since it's now on nuxt org) to better fullfill Nuxt needs.

I also suggest that for vue-meta 2.0, metaInfo is renamed to head to avoid any confusions later on.

@pimlie what will be your vision for vue-meta 2 and the breaking changes your already have in mind?

pimlie commented 5 years ago

@Atinux We should also probably decide whether we first want to release a refactored version within the vue-meta v1 branch (and maybe add deprecation warnings already if we have decided on those). Because in my mind vue-meta v2 will more be a rewrite then a refactor.

Components

As mentioned above we would like to use components so vue-meta eg doesnt need to render its own html. For this to work we would need to mount two additional root components for resp. head and body-scripts. As the head element doesnt have any possible container elements (except for maybe <noscript>) we would need to mount head as a vue component. This has the downside that we need to parse existing head content to prevent missing elements on re-render. I have made a proof-of-concept for this and general behaviour here: https://github.com/pimlie/vue-meta/tree/feat-proposal/examples/prop-2.0

Here are some more considerations with regards to these proposed changes and new structure

Considerations

Deep merging

Besides using components I would also like to remove the need for deep-merging. A possible solution for this is to just dont merge ourselves but provide a callback so the user can implement different strategies for which value needs to be set at the time a change occurs. A proof-of-concept for this functionality is here where updateMetaInfo is the callback (ignore the name vuehooks, its based on Vue.observable). But this could be an issue if the value of metaInfo.<key> is an array, eg when you are adding some general meta elements for which no specific component exists. Need to have a look at that later

Status / feedback

Curious to find out what you think about these suggestions! Do you see any caveats with the suggestions above? Please let us know!

Timkor commented 5 years ago

@pimlie Maybe interesting to note that Vue 3.0 will support portals natively.

koresar commented 5 years ago

If I may. The largest problem I had with vue-meta is copy paste. Plenty of copy paste. I had to configure meta tags for every social network (twitter is the most painful) three times in 12 months.

Is this issue a right place to ask a configuration option like this:

twitter: true

and then it just works...

(sorry, I might don't know what I'm talking about)

manniL commented 5 years ago

@koresar I know what you mean. :see_no_evil:

If you have any ideas to improve, please add them here or in the vue-meta repo :relaxed:

cesasol commented 5 years ago

There is also always a need for structured data from Schema.org, it is really annoying to be writing the complete structure every time you need it, for example a website with blog, cooking recipes, author blog pages and chefs where each one of those has a similar structure but changing slightly in all of them, so I think it would be a good idea to be able to generate labels in the head from components that the developer believes for his project.

Another situation in which these components can be used is when you start having data repetition through different labels, such as having the standard canonical url plus the facebook URL, plus the one from {'@type': 'WebSite'} of Schema.org

All this also would have to use deepmerge and added as when you have several images for og: image or you want to change the description for only one of the nested objects inside the schema ld + json

Currently in my team we do this manually based on a template with a nested Map that searches and adds or modifies based on point syntax.

theprojectsomething commented 5 years ago

seconding the need for a ready event (or observable attribute) following a navigation event; to trigger once all defined attributes from all nested views !== undefined. Our use case would be primarily async attributes for SSR.

theprojectsomething commented 5 years ago

While I like the idea of a template as mentioned by various contributors above - the format looks good and for the most part says what it is (noted it doesn't include everything contained in the <head>) - I don't see it as an ideal solution.

In my experience the meta tags implemented across pages rarely change (only the values they contain), except perhaps for the addition / exclusion of some schema.org style tags defined by content type, etc. Because of this the format is likely to be overwhelmingly redundant across components, save for a variable changed here or there.

Also important (and as mentioned by @koresar), the same value is more often than not shared across multiple meta tags. So title might be used in <title>, <meta property="og:title"> and <meta itemprop="name">, similarly with description, image, etc. This further adds to redundancy / bloat. One solution might be to allow defining multiple tags per content value/type, however this would be better suited to a variable setup, as per the current implementation - perhaps with a single master <head> template (tho even this seems unnecessary).

Finally, would I be wrong in suggesting that meta data, at least in the head of the document, is generally tied explicitly to the url? Of course there are edge cases, say games running in the title, or applications where state isn't necessarily reflected by url ... I'm sure accessibility could be argued. But in browser and search-engine land, it feels like the url should be the default case for defining distinct meta states...

... Given the above, could meta tags be more closely tied to the router than a component - controlled by stateful data (e.g. Vuex) or events?

pimlie commented 5 years ago

@koresar Agreed, re-using titles and descriptions for eg og: stuff is at the top of the wishlist :+1:

@cesasol Could you maybe share the implementation you currently use? I am also interested to add this (eg breadcrumbs are also a nice use-case), so having an idea what currently works for you might be helpful :)

@theprojectsomething You might want to take a look at the release candidate for vue-meta 2.0, it adds a refreshOnceOnNavigation option and afterNavigation callback which should already tie meta tags closer to the router while still leaving full reactivity and thus flexibility without restrictions.

Although at the moment vue-meta only supports updating using a component property, that prop is only required on one of all your components. Eg you can easily implement that property only on your root component and have it return stateful data from Vuex or from some flow of events. Although it might be nicer to support that directly in vue-meta (and we will certainly have a look at that), I think your suggestion is already possible for 99% :smile:

pimlie commented 5 years ago

For vue-meta v3 I think it would be interesting to move to a mono-repository consisting of the following packages (but list is subjected to new insights :smile: )

atinux commented 5 years ago

I really like the idea @pimlie

Would like to have @pi0 insights on this too.

atinux commented 5 years ago

I will actually rename @vue-meta/vue-app to @vue-meta/components

pimlie commented 5 years ago

Actually I am thinking about / trying to come up with a more pluggable configuration (eg looking at webpack for inspiration). I would like to be able to split components per type eg:

If you load all those pkgs then you should be able to use either one of these:

<meta name="description" content="description">
<html-description value="description">
<og-description value="description">
<twitter-description value="description">
<!-- // and in preparation for Vue v3 -->
<description value="description" html="true" og="true" twitter="true">

But adding an abstraction on top of html components like this is probably only useful when we will also use Vue to render them (at least client side). And we have take this into account before we decide on that: https://github.com/nuxt/vue-meta/pull/394

--edit-- See https://github.com/nuxt/vue-meta/pull/395 for a first implementation of the above but without splitting it into separate packages

connecteev commented 5 years ago

@pimlie what is the advantage of splitting it like that? It adds another unnecessary layer of complexity imo. Imagine adding meta tags for a few things and then realizing you need another import to handle twitter, etc...how annoying would that be?

pimlie commented 5 years ago

@connecteev fair point, as often the main reason would be separation of concerns. E.g. we could still provide a vue-meta package which already has dependencies on all the possible component packages so you wont have to worry about dependencies. That said, not sure yet if we will really go that way. It might be a bit overkill indeed.

E.g. the main thing I am still struggling with is how to use those tags. There is an initial pr for using a custom-block in vue-loader so you can write a <head> section in your components (see example in my previous comment). But with the current approach these components are first parsed to AST and then into a metaInfo object so we can merge all components together. I am just not really sure this approach provides a real benefit except causing a lot of overhead (mostly for me/us providing this functionality). If we are going to parse those components to a metaInfo object anyway, then why not just let the user write that metaInfo object in the first place? The only reason I can think of is those tags might a bit better readable, but is that really worth all the trouble?

Would appreciate some feedback on that last part ;)

connecteev commented 5 years ago

@pimlie

If we are going to parse those components to a metaInfo object anyway, then why not just let the user write that metaInfo object in the first place?

I agree with this. The more we deviate from vue (after all, nuxt is a vue framework), the worse it gets because developers have to now learn 2 formats - one for vue, and one for how nuxt interprets it. So yes, the closer we get to the vue conventions (and push the vue community to adopt better conventions if the current ones dont make sense), the better off everyone will be. Another (unrelated, but relevant to the point i'm making) example is https://github.com/nuxt/nuxt.js/issues/6102 Just my humble 2c :)

hecktarzuli commented 5 years ago

It would be really nice to have access to the current metas in plugins and not just a ref to the root instance. We've had to resort to a mixin that is used between layouts and page comps and it would really be nice to separate some of it's logic into a plugin. @pimlie :)

pimlie commented 5 years ago

and not just a ref to the root instance

What exactly do you mean with that? Can you show an example of the api you need?

hecktarzuli commented 5 years ago

Right now in the context object you get app.head. The problem with this is, if you do anything with it you are changing the root vue instance. If I had a server side plugin that did app.head.meta.push({ name: 'viewport', content: 'width=device-width, initial-scale=1' }). What happens in prod mode, is that tag gets added to the stack on every hit. So after 10 visits to the website, you have 10 viewport tags instead of one.

It would be great if either a) app.head was a clone of the root metas and was created on every request (like it acts in dev mode) or b) there was another property or method passed to the context object that is the current metas for the hit that's getting rendered.

hecktarzuli commented 4 years ago

@pimlie does that make sense?

koresar commented 4 years ago

Hello people.

Replying to this:

If you have any ideas to improve, please add them here or in the vue-meta repo ☺️

I have this, probably crazy radical, idea. Some of you might also had it.

Where the idea is coming from.

Recently I had to refactor a configuration-driven backend system. The huge problem with it is that any configuration (or similar DSL) is not scaleable, hard to maintain. If there is a lot of configuration then your system is screwed.

As part of that refactoring I replaced massive 100-1000s config files with 10 lines of JavaScript each. As the result the configuration shrunk from ~2500 lines of code to ~270. Significant improvement. Isn't it? So here is why I came to the following conclusion - the "functional" way is always better than any DSL. Examples:

The idea

The <head> is a HTML tag with a list of tags. Modern days HTML is rendered using the functional (components) approach - React/Vue/etc.

Then why are we trying to render HTML tags with config JSON (essentially a DSL) rather than functions? We already render <body> with components. Let's render <head> with components too.

I envision <head> to be rendered this way (really inventing the syntax as I type):

A Nuxt.js page:

<template>
    <Whatever/>
</template>

<script>
export default {
    name: "MyPage",
    head() {
       return (
         <MyHead main-title="Example.com website">
            <SeoMeta :twitter="true" :og="true" :facebook="true" content="My example site page" />
          </MyHead>
       );
};
</script>

<style scoped>
</style>

Or, alternatively, to avoid some repetitive code it can be like this

Same Nuxt.js page:

<template>
    <Whatever/>
</template>

<head main-title="Example.com website">
    <SeoMeta :twitter="true" :og="true" :facebook="true" content="My example site page" />
</head>

<script>
export default {
    name: "MyPage"
};
</script>

<style scoped>
</style>

You see where I'm going?

Similar things already exist in React world (I've just googled them).

  1. React portals
  2. react-helmet
koresar commented 4 years ago

Ignore my previous message. I've just managed to read the very first message by Sebastien.

Yeah, <n-head> sounds great.

homer

pimlie commented 4 years ago

@hecktarzuli Thanks for explaining. It makes sense partially I guess, but I think its beyond the scope of vue-meta to facilitate any user pattern. The issue you are describing seems to be caused by Vue's runInNewContext option. In my mind it would make sense that this should be fixed in user land as not every user will need this and cloning an object comes at a cost. Or maybe we could just add some default library method to help you with it (similar to as how you can call Vue.use multiple times but Vue will only install a plugin once normally), feel free to create a PR for that 😉

@koresar Thanks for the nice idea! I've made a (now stale, so you couldnt know unfortunately) proof of concept PR that does this more or less: https://github.com/nuxt/vue-meta/pull/392 The issue seemed to be that normally the Vue app lives in the body, so we cant use the normal Vue-app for parsing the components to html. We could run a separate Vue-app for the head, but instantiating 2 Vue-app's instead of one comes at a measurable performance/startup cost. Vue also knows portals, but at least in Vue2 they dont work for SSR and its not sure yet if/how they will work for Vue3 ssr (afaik). So thats why the above linked PR first falls back to parsing the vue-loader custom head block into a JSON object and then use that for manually generating the head tags string which then can be injected into the html. Feel free to tinker with it if you know a better implementation 😸

troxler commented 3 years ago

Hi! I'm the author of headful (npm) and vue-headful (npm). I only just found this issue now even though headful was already mentioned in this issue. I appreciate that you are considering to implement features from it :)

FWIW, I consider deprecating vue-headful in favour of vue-meta.

AlexVipond commented 3 years ago

Inspired by this conversation, I just published the first version of useHead, a composition function you can use to add and update <head> content from inside the setup function in a Vue 3 component. Example use (Vue Router stuff is optional, just included for the sake of example):

import { useHead } from '@baleada/vue-features'
import { useRoute } from 'vue-router'
import { context } from '/path/to/global-store'

export default {
  setup () {
    const route = useRoute()
    useHead({
      title: computed(() => context.article.frontMatter?.title ?? SITE_NAME),
      metas: [
        { 
          property: 'og:title',
          content: computed(() => context.article.frontMatter?.title ?? SITE_NAME)
        },
        { 
          property: 'og:description',
          content: computed(() => context.article.frontMatter?.summary ?? '')
        },
        { 
          property: 'og:image',
          content: computed(() => context.article.frontMatter?.image ?? '')
        },
        { 
          property: 'og:url',
          content: computed(() => route.fullPath),
        },
      ]
    })
  }
}

Under the hood, I had to reimplement a few Vue features:

As others in this issue have noted, those actions are easily available when you're working with a Vue template, but they're not accessible from inside composition functions.

I have a feature request open in the Vue 3 repo asking the Vue team to give us lower-level APIs for patching DOM attributes and creating nodes in the Virtual DOM, then relying on Vue to schedule renderer updates efficiently. In the meantime, I wrote a bunch of composition-function-friendly reimplementations of Vue template features like v-bind and v-if. That's what I'm using under the hood of useHead.

This experience has convinced me that composition functions are the right choice for vue-meta's behavior. Once the basic logic is implemented in a composition function, it's very easy to wrap a renderless component around that composition function, and offer the component to anyone who prefers that component-based API. It's also very possible to write a Vue plugin that adds a metaInfo component option, while powering the entire plugin with that same composition function.

If anyone wants to use useHead or any other code I've linked here, let me know! Nothing is typed or documented yet, but I can show additional usage examples to anyone who would like them.

atinux commented 3 years ago

Thank you @AlexVipond

I would love to see a repo even without docs

AlexVipond commented 3 years ago

@Atinux Sure thing! Here's the source for the now-published useHead function: https://github.com/baleada/vue-features/blob/main/src/features/useHead.js

Note that only meta and title tags are supported, as that's all I personally needed when I wrote it. Support for link, noscript, etc. wouldn't be difficult to add.

I haven't deeply explored server-side rendering with this composition function, so I'm unsure of how fully it solves SEO in Vue.

chriscalo commented 3 years ago

Related: the API in vue-head feels perfect to me because it so closely resembles plain HTML (much like the original proposal):

<template>
  <Head>
    <title>Hello Vue</title>
    <meta name="description" content="Do you like it?" />
  </Head>
</template>

<script>
import { Head } from '@egoist/vue-head'

export default {
  components: {
    Head,
  },
}
</script>

It also makes simple work of controling the HTML generated by SSR output:

import { createApp, h, Fragment } from 'vue'
import { renderToString } from '@vue/server-renderer'

const app = createApp()
const appHTML = await renderToString(app)
const headHTML = await renderToString(
  h(Fragment, app.config.globalProperties.$head.headTags)
)

const finalHTML = `
<html>

<head>${headHTML}</head>

<body>${appHTML}</body>

</html>
`

This <n-head> should have some props to handle body-attrs and html-attrs, about head-attrs, well, it’s all the others non-defined props directly :)

What about the following API so everything feels close to plain HTML?

<template>
  <Html lang="en"/>
  <Head>
    <title>Hello Vue</title>
    <meta name="description" content="Do you like it?" />
  </Head>
  <Body class="foo"/>
</template>
atinux commented 3 years ago

We discussed about something like this for Nuxt 3 with @pi0, we are going to explore and keep this thread updated as soon as we have something

Simonl9l commented 2 years ago

per @znck comment above regarding webpack, can we please ensure that vite is also considered from a plugin perspective.