diff --git a/packages/@vuepress/core/lib/client/index.ssr.html b/packages/@vuepress/core/lib/client/index.ssr.html index 1c5e66bc8c..351c008efe 100644 --- a/packages/@vuepress/core/lib/client/index.ssr.html +++ b/packages/@vuepress/core/lib/client/index.ssr.html @@ -4,7 +4,6 @@ {{ title }} - {{{ userHeadTags }}} {{{ pageMeta }}} diff --git a/packages/@vuepress/core/lib/client/root-mixins/updateMeta.js b/packages/@vuepress/core/lib/client/root-mixins/updateMeta.js index bfa818be2d..5b5228c138 100644 --- a/packages/@vuepress/core/lib/client/root-mixins/updateMeta.js +++ b/packages/@vuepress/core/lib/client/root-mixins/updateMeta.js @@ -1,15 +1,26 @@ +import unionBy from 'lodash/unionBy' + export default { + // created will be called on both client and ssr created () { + this.siteMeta = this.$site.headTags + .filter(([headerType]) => headerType === 'meta') + .map(([_, headerValue]) => headerValue) + if (this.$ssrContext) { + const mergedMetaItems = this.getMergedMetaTags() + this.$ssrContext.title = this.$title this.$ssrContext.lang = this.$lang - this.$ssrContext.description = this.$page.description || this.$description + this.$ssrContext.pageMeta = renderPageMeta(mergedMetaItems) } }, - + // Other life cycles will only be called at client mounted () { + // init currentMetaTags from DOM + this.currentMetaTags = [...document.querySelectorAll('meta')] + // update title / meta tags - this.currentMetaTags = new Set() this.updateMeta() }, @@ -17,22 +28,17 @@ export default { updateMeta () { document.title = this.$title document.documentElement.lang = this.$lang - const userMeta = this.$page.frontmatter.meta || [] - const meta = userMeta.slice(0) - const useGlobalDescription = userMeta.filter(m => m.name === 'description').length === 0 - - // #665 Avoid duplicate description meta at runtime. - if (useGlobalDescription) { - meta.push({ name: 'description', content: this.$description }) - } - // Including description meta coming from SSR. - const descriptionMetas = document.querySelectorAll('meta[name="description"]') - if (descriptionMetas.length) { - descriptionMetas.forEach(m => this.currentMetaTags.add(m)) - } + const newMetaTags = this.getMergedMetaTags() + this.currentMetaTags = updateMetaTags(newMetaTags, this.currentMetaTags) + }, - this.currentMetaTags = new Set(updateMetaTags(meta, this.currentMetaTags)) + getMergedMetaTags () { + const pageMeta = this.$page.frontmatter.meta || [] + // pageMetaTags have higher priority than siteMetaTags + // description needs special attention as it has too many entries + return unionBy([{ name: 'description', content: this.$description }], + pageMeta, this.siteMeta, metaIdentifier) } }, @@ -47,14 +53,20 @@ export default { } } -function updateMetaTags (meta, current) { - if (current) { - [...current].forEach(c => { +/** + * Replace currentMetaTags with newMetaTags + * @param {Array} newMetaTags + * @param {Array} currentMetaTags + * @returns {Array} + */ +function updateMetaTags (newMetaTags, currentMetaTags) { + if (currentMetaTags) { + [...currentMetaTags].forEach(c => { document.head.removeChild(c) }) } - if (meta) { - return meta.map(m => { + if (newMetaTags) { + return newMetaTags.map(m => { const tag = document.createElement('meta') Object.keys(m).forEach(key => { tag.setAttribute(key, m[key]) @@ -64,3 +76,35 @@ function updateMetaTags (meta, current) { }) } } + +/** + * Try to identify a meta tag by name, property or itemprop + * + * Return a complete string if none provided + * @param {Object} tag from frontmatter or siteMetaTags + * @returns {String} + */ +function metaIdentifier (tag) { + for (const item of ['name', 'property', 'itemprop']) { + if (tag.hasOwnProperty(item)) return tag[item] + item + } + return JSON.stringify(tag) +} + +/** + * Render meta tags + * + * @param {Array} meta + * @returns {Array} + */ + +function renderPageMeta (meta) { + if (!meta) return '' + return meta.map(m => { + let res = ` { + res += ` ${key}="${m[key]}"` + }) + return res + `>` + }).join('\n ') +} diff --git a/packages/@vuepress/core/lib/node/App.js b/packages/@vuepress/core/lib/node/App.js index 2a6cdc261b..a6d17320ff 100755 --- a/packages/@vuepress/core/lib/node/App.js +++ b/packages/@vuepress/core/lib/node/App.js @@ -438,6 +438,7 @@ module.exports = class App { title: this.siteConfig.title || '', description: this.siteConfig.description || '', base: this.base, + headTags: this.siteConfig.head || [], pages: this.pages.map(page => page.toJson()), themeConfig: this.siteConfig.themeConfig || {}, locales @@ -499,4 +500,3 @@ module.exports = class App { return this } } - diff --git a/packages/@vuepress/core/lib/node/build/index.js b/packages/@vuepress/core/lib/node/build/index.js index 271af8843e..183d3b97b7 100644 --- a/packages/@vuepress/core/lib/node/build/index.js +++ b/packages/@vuepress/core/lib/node/build/index.js @@ -77,9 +77,11 @@ module.exports = class Build extends EventEmitter { }) // pre-render head tags from user config + // filter out meta tags for they will be injected in updateMeta.js this.userHeadTags = (this.context.siteConfig.head || []) + .filter(([headTagType]) => headTagType !== 'meta') .map(renderHeadTag) - .join('\n ') + .join('\n ') // if the user does not have a custom 404.md, generate the theme's default if (!this.context.pages.some(p => p.path === '/404.html')) { @@ -134,14 +136,9 @@ module.exports = class Build extends EventEmitter { async renderPage (page) { const pagePath = decodeURIComponent(page.path) - // #565 Avoid duplicate description meta at SSR. - const meta = (page.frontmatter && page.frontmatter.meta || []).filter(item => item.name !== 'description') - const pageMeta = renderPageMeta(meta) - const context = { url: page.path, userHeadTags: this.userHeadTags, - pageMeta, title: 'VuePress', lang: 'en', description: '', @@ -221,24 +218,6 @@ function renderAttrs (attrs = {}) { } } -/** - * Render meta tags - * - * @param {Array} meta - * @returns {Array} - */ - -function renderPageMeta (meta) { - if (!meta) return '' - return meta.map(m => { - let res = ` { - res += ` ${key}="${escape(m[key])}"` - }) - return res + `>` - }).join('') -} - /** * find and remove empty style chunk caused by * https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85