diff --git a/lib/common/utils.js b/lib/common/utils.js index 997515c721..54a13cbaa1 100644 --- a/lib/common/utils.js +++ b/lib/common/utils.js @@ -151,24 +151,6 @@ export function flatRoutes (router, path = '', routes = []) { return routes } -export function attrsStr (attrObj = {}, exclude = []) { - return Object.keys(attrObj) - .filter(attr => !exclude.includes(attr)) - .map(attr => { - if (typeof attrObj[attr] !== 'string') { - return attr - } - - const val = attrObj[attr].replace('"', '\'') - - if (attr === 'hid') { - attr = 'data-hid' - } - - return `${attr}="${val}"` - }).join(' ') -} - export function cleanChildrenRoutes (routes, isChild = false) { let start = -1 let routesIndex = [] diff --git a/lib/core/meta.js b/lib/core/meta.js index 75f8b78a86..0c3fdff70a 100644 --- a/lib/core/meta.js +++ b/lib/core/meta.js @@ -1,5 +1,7 @@ -import { attrsStr } from 'utils' +import Vue from 'vue' +import VueMeta from 'vue-meta' +import VueServerRenderer from 'vue-server-renderer' import LRU from 'lru-cache' export default class MetaRenderer { @@ -7,68 +9,70 @@ export default class MetaRenderer { this.nuxt = nuxt this.renderer = renderer this.options = nuxt.options + this.vueRenderer = VueServerRenderer.createRenderer() this.cache = LRU({}) + + // Add VueMeta to Vue (this is only for SPA mode) + // See lib/app/index.js + Vue.use(VueMeta, { + keyName: 'head', + attribute: 'data-n-head', + ssrAttribute: 'data-n-head-ssr', + tagIDKeyName: 'hid' + }) } - render ({ url = '/' }) { - let head = this.cache.get(url) - - if (head) { - return head - } - - head = '' - - // Title - if (typeof this.options.head.title === 'string') { - head += `${this.options.head.title || ''}` - } - - // Meta - if (Array.isArray(this.options.head.meta)) { - this.options.head.meta.forEach(meta => { - head += `` + getMeta(url) { + return new Promise((resolve, reject) => { + const vm = new Vue({ + render: (h) => h(), // Render empty html tag + head: this.options.head || {} }) - } - - // Links - if (Array.isArray(this.options.head.link)) { - this.options.head.link.forEach(link => { - head += `` + this.vueRenderer.renderToString(vm, (err) => { + if (err) return reject(err) + resolve(vm.$meta().inject()) }) + }) + } + + async render ({ url = '/' }) { + let meta = this.cache.get(url) + + if (meta) { + return meta } - // Style - if (Array.isArray(this.options.head.style)) { - this.options.head.style.forEach(style => { - head += `` - }) + meta = { + HTML_ATTRS: '', + BODY_ATTRS: '', + HEAD: '' } - - // Script - if (Array.isArray(this.options.head.script)) { - this.options.head.script.forEach(script => { - head += `` - }) - } - + // Get vue-meta context + const m = await this.getMeta(url) + // HTML_ATTRS + meta.HTML_ATTRS = 'data-n-head-ssr ' + m.htmlAttrs.text() + // BODY_ATTRS + meta.BODY_ATTRS = m.bodyAttrs.text() + // HEAD tags + meta.HEAD = m.meta.text() + m.title.text() + m.link.text() + m.style.text() + m.script.text() + m.noscript.text() // Resource Hints const clientManifest = this.renderer.resources.clientManifest if (this.options.render.resourceHints && clientManifest) { const publicPath = clientManifest.publicPath || '/_nuxt/' // Pre-Load initial resources if (Array.isArray(clientManifest.initial)) { - head += clientManifest.initial.map(r => ``).join('') + meta.HEAD += clientManifest.initial.map(r => ``).join('') } // Pre-Fetch async resources if (Array.isArray(clientManifest.async)) { - head += clientManifest.async.map(r => ``).join('') + meta.HEAD += clientManifest.async.map(r => ``).join('') } } - this.cache.set(url, head) + // Set meta tags inside cache + this.cache.set(url, meta) - return head + return meta } } diff --git a/lib/core/meta.old.js b/lib/core/meta.old.js new file mode 100644 index 0000000000..75f8b78a86 --- /dev/null +++ b/lib/core/meta.old.js @@ -0,0 +1,74 @@ + +import { attrsStr } from 'utils' +import LRU from 'lru-cache' + +export default class MetaRenderer { + constructor (nuxt, renderer) { + this.nuxt = nuxt + this.renderer = renderer + this.options = nuxt.options + this.cache = LRU({}) + } + + render ({ url = '/' }) { + let head = this.cache.get(url) + + if (head) { + return head + } + + head = '' + + // Title + if (typeof this.options.head.title === 'string') { + head += `${this.options.head.title || ''}` + } + + // Meta + if (Array.isArray(this.options.head.meta)) { + this.options.head.meta.forEach(meta => { + head += `` + }) + } + + // Links + if (Array.isArray(this.options.head.link)) { + this.options.head.link.forEach(link => { + head += `` + }) + } + + // Style + if (Array.isArray(this.options.head.style)) { + this.options.head.style.forEach(style => { + head += `` + }) + } + + // Script + if (Array.isArray(this.options.head.script)) { + this.options.head.script.forEach(script => { + head += `` + }) + } + + // Resource Hints + const clientManifest = this.renderer.resources.clientManifest + if (this.options.render.resourceHints && clientManifest) { + const publicPath = clientManifest.publicPath || '/_nuxt/' + // Pre-Load initial resources + if (Array.isArray(clientManifest.initial)) { + head += clientManifest.initial.map(r => ``).join('') + } + + // Pre-Fetch async resources + if (Array.isArray(clientManifest.async)) { + head += clientManifest.async.map(r => ``).join('') + } + } + + this.cache.set(url, head) + + return head + } +} diff --git a/lib/core/renderer.js b/lib/core/renderer.js index 11b201e44e..c1f4059943 100644 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -10,7 +10,7 @@ import _ from 'lodash' import { join, resolve } from 'path' import fs from 'fs-extra' import { createBundleRenderer } from 'vue-server-renderer' -import { getContext, setAnsiColors, isUrl, attrsStr } from 'utils' +import { getContext, setAnsiColors, isUrl } from 'utils' import Debug from 'debug' import Youch from '@nuxtjs/youch' import { SourceMapConsumer } from 'source-map' @@ -446,13 +446,12 @@ export default class Renderer extends Tapable { // Basic response if SSR is disabled or spa data provided const spa = context.spa || (context.res && context.res.spa) if (this.noSSR || spa) { - const HEAD = this.metaRenderer.render(context) - const HTML_ATTRS = attrsStr(this.options.head.htmlAttrs) + const { HTML_ATTRS, BODY_ATTRS, HEAD } = await this.metaRenderer.render(context) const APP = `
${this.resources.loadingHTML}
` const data = { HTML_ATTRS, - BODY_ATTRS: '', + BODY_ATTRS, HEAD, APP }