vuejs / vuepress

πŸ“ Minimalistic Vue-powered static site generator
https://vuepress.vuejs.org
MIT License
22.43k stars 4.79k forks source link

Set Metadata on component level #2153

Open samanthaming opened 4 years ago

samanthaming commented 4 years ago

Feature request

Currently the way to set custom metadata is through the frontmatter in the markdown file. It'd be very helpful if we could set this on the component level.

The Problem

Here's a scenario of why this is important. I use Cloudinary to store my images and I use a gulp task to upload the images. Which then populates a single javascript object with all of the ID of the images. These are the images I want to use in my meta head (especially for my social media links on twitter and facebook). And I really don't want to set them manually in each of my markdown frontmatter.

Ideal Solution

It'd be so nice, if I could set the meta head directly in my layout or component. Something like vue-meta from Nuxt.

Alternative Solution

Unfortunately, I had trouble trying to figure out how to set the vue-meta plugin to work with VuePress via the SSR way. So instead I created a custom plugin that will programmatically set the frontmatter with frontmatter.meta. It's working in my dev environment, however, when the site is built, it then has duplicates (I just found this out and have not looked into it yet, when I find out why, I'll update this issue).

Conclusion

I know VuePress is mainly built for technical documentation. But for those using it for more than that, I think meta data is a super important feature. I used VuePress to build my blog and I picked a static site generator for SEO reason. And meta data is an integral part of that.

Overall I loved using VuePress to build my new site (the markdown support and the built-in header search are amazing!). All that's missing is this meta data support. Not trying to compare VuePress to others, but Gridsome offers this. And it's probably one of the feature that might make me want to switch over.

Anyways, I think the team did a fabulous job on this. I hope you will consider this feature! πŸ‘

bencodezen commented 4 years ago

Thanks for filing this @samanthaming! We really appreciate the time and effort you took to investigate this and will add this our list of items to discuss for our roadmap! Would you be interested in code pairing sometime so I can get a better understanding of the problem and your current solution?

samanthaming commented 4 years ago

Thanks @bencodezen for looking into this πŸ‘ Absolutely! I'll reach out to you and we can set something up πŸ™Œ

adamdehaven commented 3 years ago

To follow up on @samanthaming's comment, I also wrote a custom plugin that injects metadata into the ssrTemplate - would be glad to share, if helpful.

samanthaming commented 3 years ago

@adamdehaven wooo you have a custom plugin 🀩 is it an open source that you can provide a link to? Would love to check it out 😊

adamdehaven commented 3 years ago

@samanthaming I haven't released it yet, but plan to (still doing a little testing). I just redesigned my site with VuePress as well and am getting ready to push the site itself live in the coming days.

The plugin I wrote injects the canonical URL, Schema.org structured data, as well as extra metadata into the ssrTemplate during the build process (so the tags will not be present when running npm run docs:dev)

adamdehaven commented 3 years ago

@bencodezen I may (huge question mark here) have found a somewhat easy way to implement part of the ask here (canonical URL tag), which could likely be expanded to allow for dynamic injection.

If the check for headerType was updated in vuepress/packages/@vuepress/core/lib/client/root-mixins/updateMeta.js as shown below, I believe the canonical URL could be injected along with the rest of the metadata in the frontmatter.

// vuepress/packages/@vuepress/core/lib/client/root-mixins/updateMeta.js

export default {
  // created will be called on both client and ssr
  created () {
    this.siteMeta = this.$site.headTags
      // .filter(([headerType]) => headerType === 'meta') // OLD
      .filter(([headerType]) => ['meta', 'link'].includes(headerType)) // NEW: allows for 'meta' and 'link' tags 
      .map(([_, headerValue]) => headerValue)

      // ... more code

Then, in a page's frontmatter:

---

# Defining page-level metadata - unchanged (goes back to original request to allow for dynamic metadata)
meta: 
    - name: description
      content: Page description text

# Defining other link data (although the requested link in Issues is currently canonical URL)
link:
    - href: https://www.example.com/canonical/url/link/
      rel: canonical

---

This still requires the values be hard-coded into the frontmatter; however, is an easy way to expand the type of tag that can be injected via frontmatter.

The problem with other canonical URL solutions (as well as injecting metadata) is that they only load for the current page and then are not updated dynamically (as in the build process, not client-side) as the pages change. The correct URLs display in the /dist/ directory pages after build; however, the VuePress SPA never refreshes the attributes when moving from page to page. As an example, the code below probably looks familiar:

created() {
    if (typeof this.$ssrContext !== "undefined") {
        this.$ssrContext.userHeadTags += `\n    <link rel="canonical" href="${this.computeURL()}"/>`;
    }
}

This only loads the proper URL (or other content) for the first-requested page. Any subsequent navigation within the VuePress SPA does not update the properties, even if you watch $page or any of the other hacks that are out there.

bencodezen commented 3 years ago

@adamdehaven Great research! Would you be interested in opening up a PR on this?

In the long run, we probably need to find a way to integrate vue-meta as its API is fairly ubiquitous in the Vue community these days. Hoping to get to this at some point, but your solution seems to be a great way to open up possibilities in the meantime. Let me know what you think!

adamdehaven commented 3 years ago

@bencodezen integrating vue-meta would be great, but I'm not familiar what all it allows you to do... I'll have to take a look.

I should be able to submit a PR; however, I'm traveling and it may be next week.

Question: would implementing the way I suggested above also open up the properties to the extendPageData method in the Option API? If yes, then I'll probably create the PR to also include script tags, which will also allow for injecting Schema.org structured data into the page as well! πŸŽ‰

If I allow script tags as well, and wanted to include JSON as the content (instead of adding an attribute) how would this be structured in YAML?

bencodezen commented 3 years ago

@adamdehaven No worries as far as urgency. In the event I manage to get a head start, I'll be sure to update the thread so you're aware πŸ‘

In regards to your question, I inherited the project after its release, so I'm still learning the ins and outs of the codebase. If you discover the answer to this before I do, would love to get your findings documented so other people have an easier time contributing in the future!

d-pollard commented 3 years ago

Started my cursory overview of this and had a few quick questions here:

Would we want to implement vue-meta here first, or come up with a stopgap?

If moving to vue-meta is the answer, do we try and incorporate its API into our existing frontmatter, or do we push to use vue-meta and introduce breaking changes?

If the answer isn't vue-meta, then would we try to implement our own API? I'm not too keen on this one as it seems to reinvent the wheel with the added bonus maintenance for the VuePress team

Any and all feedback/dialogue is welcome!

adamdehaven commented 3 years ago

@d-pollard I think implementing vue-meta may be the way to go; however, the big piece here is to allow that data to be available in the DOM immediately (as is the case with current meta info) instead of it having to render along with the rest of the Vue components. I have a custom plugin I wrote (similar to @samanthaming) where I dynamically inject the metadata; however, the vue-meta implementation wouldn't account for canonical URLs, structured data, or as I understand how it would work with VuePress, allowing for the tags to be available for SEO purposes (e.g. site scrapers, robots, etc.)

d-pollard commented 3 years ago

@adamdehaven - thanks for the feedback

From what I've read on vue-meta, it seems like they come with SSR support so I believe it'd be available straight away in the DOM

I'm concerned, however, with how we'd introduce vue-meta then, since we already have meta information coming from frontmatter, would we try to build on top of that API with vue-meta (something I'm not sure is possible) or would we move the meta information away from frontmatter and try to use a special component that binds to vue-meta?

adamdehaven commented 3 years ago

@d-pollard so just thinking out loud, but here goes:

What if VuePress included vue-meta available to turn on/off via a setting in the .vuepress/config.js?

If VuePress does do something to this effect, I strongly plead for a way to also include a page's canonical URL in the frontmatter so that it can be injected directly into each page during SSR so it's available in the DOM (and, this would hopefully be compatible if using the VuePress Blog Plugin as well). With AMP pages, article syndication (e.g. Medium, Dev.to, etc.) the canonical URL tag is super-important. I realize this would likely be a separate feature/PR, I believe it could likely be integrated in a similar fashion to what is trying to be solved for here πŸ™Œ

Edit: spelling is hard.

d-pollard commented 3 years ago

@adamdehaven - since the canonical bit was a quick win here, I've opened this PR: https://github.com/vuejs/vuepress/pull/2658

Mind reviewing it and giving your thoughts for me?

adamdehaven commented 3 years ago

~@d-pollard from what I've tested so far, PR #2658 does not work. The canonical link tag is added to the first page that contains the corresponding frontmatter entry; however, the value of the link tag does not update when the route changes.~

~For example, using the official @vuepress/plugin-blog on my site, when I add the changes in the PR, the canonical URL is correct on the first page /about/ but does not update on route change to /blog/post-slug/.~

~(Same behavior occurs without using the blog plugin as well)~

Update: Moving comments to PR #2658

d-pollard commented 3 years ago

@adamdehaven - my apologies, I forgot to commit my most recent change; please try again with the newest changes

adamdehaven commented 3 years ago

@d-pollard moving the PR conversation to #2658 thread πŸ˜„

d-pollard commented 3 years ago

@adamdehaven @samanthaming

This PR introduces Vue Meta: https://github.com/vuejs/vuepress/pull/2661

it is a backwards compatible replacement for current setup, please add any questions or concerns in that thread