diff --git a/examples/server.js b/examples/server.js index fac6d742..170af38e 100644 --- a/examples/server.js +++ b/examples/server.js @@ -32,11 +32,12 @@ app.use(express.static(__dirname)) app.use(async (req, res, next) => { if (!req.url.startsWith('/ssr')) { - next() + return next() } try { - const html = await renderPage() + const context = { url: req.url } + const html = await renderPage(context) res.send(html) } catch (e) { consola.error('SSR Oops:', e) diff --git a/examples/ssr/App.js b/examples/ssr/App.js index 808b49b3..ae55f948 100644 --- a/examples/ssr/App.js +++ b/examples/ssr/App.js @@ -1,32 +1,82 @@ import Vue from 'vue' +import Router from 'vue-router' import VueMeta from '../../' +Vue.use(Router) Vue.use(VueMeta, { tagIDKeyName: 'hid' }) export default function createApp () { - return new Vue({ - components: { - Hello: { - template: '

Hello World

', - metaInfo: { - title: 'Hello World', - meta: [ - { - hid: 'description', - name: 'description', - content: 'The description' - } - ] + const Home = { + template: `
+ About + +

Hello World

+
`, + metaInfo: { + title: 'Hello World', + meta: [ + { + hid: 'og:title', + name: 'og:title', + content: 'Hello World' + }, + { + hid: 'description', + name: 'description', + content: 'Hello World' } - } - }, + ] + } + } + + const About = { + template: `
+ Home + +

About

+
`, + metaInfo: { + title: 'About World', + meta: [ + { + hid: 'og:title', + name: 'og:title', + content: 'About World' + }, + { + hid: 'description', + name: 'description', + content: 'About World' + } + ] + } + } + + const router = new Router({ + mode: 'history', + base: '/ssr', + routes: [ + { path: '/', component: Home }, + { path: '/about', component: About } + ] + }) + + const app = new Vue({ + router, metaInfo () { return { title: 'Boring Title', htmlAttrs: { amp: true }, meta: [ + { + skip: this.count < 1, + hid: 'og:title', + name: 'og:title', + template: chunk => `${chunk} - My Site`, + content: 'Default Title' + }, { hid: 'description', name: 'description', @@ -73,8 +123,6 @@ export default function createApp () { }, template: `
- -

{{ count }} users loaded

+ +
` }) + + return { app, router } } diff --git a/examples/ssr/browser.js b/examples/ssr/browser.js index b10d0e5e..37a7e492 100644 --- a/examples/ssr/browser.js +++ b/examples/ssr/browser.js @@ -2,4 +2,5 @@ import createApp from './App' window.users = [] -createApp().$mount('#app') +const { app } = createApp() +app.$mount('#app') diff --git a/examples/ssr/server.js b/examples/ssr/server.js index 2d1964fb..d7334dbe 100644 --- a/examples/ssr/server.js +++ b/examples/ssr/server.js @@ -14,14 +14,27 @@ const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g }) process.server = true -export async function renderPage () { - const app = await createApp() - const appHtml = await renderer.renderToString(app) +export async function renderPage ({ url }) { + const { app, router } = await createApp() - const pageHtml = compiled({ - app: appHtml, - ...app.$meta().inject() - }) + router.push(url.substr(4)) + + return new Promise((resolve, reject) => { + router.onReady(async () => { + const matchedComponents = router.getMatchedComponents() + // no matched routes, reject with 404 + if (!matchedComponents.length) { + return reject({ code: 404 }) + } + + const appHtml = await renderer.renderToString(app) - return pageHtml + const pageHtml = compiled({ + app: appHtml, + ...app.$meta().inject() + }) + + resolve(pageHtml) + }) + }) } diff --git a/src/client/updaters/tag.js b/src/client/updaters/tag.js index 341175e4..db7c8c35 100644 --- a/src/client/updaters/tag.js +++ b/src/client/updaters/tag.js @@ -1,4 +1,4 @@ -import { booleanHtmlAttributes, commonDataAttributes } from '../../shared/constants' +import { booleanHtmlAttributes, commonDataAttributes, tagProperties } from '../../shared/constants' import { includes } from '../../utils/array' import { queryElements, getElementsKey } from '../../utils/elements.js' @@ -48,7 +48,7 @@ export default function updateTag (appId, options = {}, type, tags, head, body) for (const attr in tag) { /* istanbul ignore next */ - if (!tag.hasOwnProperty(attr)) { + if (!tag.hasOwnProperty(attr) || includes(tagProperties, attr)) { continue } diff --git a/src/server/generators/tag.js b/src/server/generators/tag.js index b6da8a17..a5c62450 100644 --- a/src/server/generators/tag.js +++ b/src/server/generators/tag.js @@ -3,6 +3,7 @@ import { tagsWithoutEndTag, tagsWithInnerContent, tagAttributeAsInnerContent, + tagProperties, commonDataAttributes } from '../../shared/constants' @@ -43,7 +44,7 @@ export default function tagGenerator ({ ssrAppId, attribute, tagIDKeyName } = {} // build a string containing all attributes of this tag for (const attr in tag) { // these attributes are treated as children on the tag - if (tagAttributeAsInnerContent.includes(attr) || attr === 'once') { + if (tagAttributeAsInnerContent.includes(attr) || tagProperties.includes(attr)) { continue } diff --git a/src/shared/constants.js b/src/shared/constants.js index abb59503..ecf4c5fc 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -92,6 +92,8 @@ export const tagsWithInnerContent = ['noscript', 'script', 'style'] // Attributes which are inserted as childNodes instead of HTMLAttribute export const tagAttributeAsInnerContent = ['innerHTML', 'cssText', 'json'] +export const tagProperties = ['once', 'template'] + // Attributes which should be added with data- prefix export const commonDataAttributes = ['body', 'pbody'] diff --git a/src/shared/getComponentOption.js b/src/shared/getComponentOption.js index ecd4e07a..e09318f2 100644 --- a/src/shared/getComponentOption.js +++ b/src/shared/getComponentOption.js @@ -1,8 +1,6 @@ import { isFunction, isObject } from '../utils/is-type' -import { findIndex } from '../utils/array' import { defaultInfo } from './constants' import { merge } from './merge' -import { applyTemplate } from './template' import { inMetaInfoBranch } from './meta-helpers' export function getComponentMetaInfo (options = {}, component) { @@ -24,7 +22,7 @@ export function getComponentMetaInfo (options = {}, component) { * @return {Object} result - final aggregated result */ export function getComponentOption (options = {}, component, result = {}) { - const { keyName, metaTemplateKeyName, tagIDKeyName } = options + const { keyName } = options const { $options, $children } = component if (component._inactive) { @@ -62,20 +60,5 @@ export function getComponentOption (options = {}, component, result = {}) { }) } - if (metaTemplateKeyName && result.meta) { - // apply templates if needed - result.meta.forEach(metaObject => applyTemplate(options, metaObject)) - - // remove meta items with duplicate vmid's - result.meta = result.meta.filter((metaItem, index, arr) => { - return ( - // keep meta item if it doesnt has a vmid - !metaItem.hasOwnProperty(tagIDKeyName) || - // or if it's the first item in the array with this vmid - index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName]) - ) - }) - } - return result } diff --git a/src/shared/getMetaInfo.js b/src/shared/getMetaInfo.js index e5d27e7c..af446d90 100644 --- a/src/shared/getMetaInfo.js +++ b/src/shared/getMetaInfo.js @@ -1,3 +1,4 @@ +import { findIndex } from '../utils/array' import { escapeMetaInfo } from '../shared/escaping' import { applyTemplate } from './template' @@ -9,6 +10,7 @@ import { applyTemplate } from './template' * @return {Object} - returned meta info */ export default function getMetaInfo (options = {}, info, escapeSequences = [], component) { + const { tagIDKeyName } = options // Remove all "template" tags from meta // backup the title chunk in case user wants access to it @@ -27,5 +29,21 @@ export default function getMetaInfo (options = {}, info, escapeSequences = [], c info.base = Object.keys(info.base).length ? [info.base] : [] } + if (info.meta) { + // remove meta items with duplicate vmid's + info.meta = info.meta.filter((metaItem, index, arr) => { + const hasVmid = metaItem.hasOwnProperty(tagIDKeyName) + if (!hasVmid) { + return true + } + + const isFirstItemForVmid = index === findIndex(arr, item => item[tagIDKeyName] === metaItem[tagIDKeyName]) + return isFirstItemForVmid + }) + + // apply templates if needed + info.meta.forEach(metaObject => applyTemplate(options, metaObject)) + } + return escapeMetaInfo(options, info, escapeSequences) } diff --git a/src/shared/merge.js b/src/shared/merge.js index 23dbe698..a251d0ea 100644 --- a/src/shared/merge.js +++ b/src/shared/merge.js @@ -10,6 +10,10 @@ export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, cont // using an O(1) lookup associative array exploit const destination = [] + if (!target.length && !source.length) { + return destination + } + target.forEach((targetItem, targetIndex) => { // no tagID so no need to check for duplicity if (!targetItem[tagIDKeyName]) { @@ -53,12 +57,17 @@ export function arrayMerge ({ component, tagIDKeyName, metaTemplateKeyName, cont } const sourceTemplate = sourceItem[metaTemplateKeyName] - if (!sourceTemplate) { // use parent template and child content applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, targetTemplate) - } else if (!sourceItem[contentKeyName]) { - // use child template and parent content + + // set template to true to indicate template was already applied + sourceItem.template = true + return + } + + if (!sourceItem[contentKeyName]) { + // use parent content and child template applyTemplate({ component, metaTemplateKeyName, contentKeyName }, sourceItem, undefined, targetItem[contentKeyName]) } }) diff --git a/src/shared/template.js b/src/shared/template.js index 340ce51b..144db827 100644 --- a/src/shared/template.js +++ b/src/shared/template.js @@ -1,13 +1,23 @@ import { isUndefined, isFunction } from '../utils/is-type' export function applyTemplate ({ component, metaTemplateKeyName, contentKeyName }, headObject, template, chunk) { - if (isUndefined(template)) { + if (template === true || headObject[metaTemplateKeyName] === true) { + // abort, template was already applied + return false + } + + if (isUndefined(template) && headObject[metaTemplateKeyName]) { template = headObject[metaTemplateKeyName] - delete headObject[metaTemplateKeyName] + headObject[metaTemplateKeyName] = true } // return early if no template defined if (!template) { + // cleanup faulty template properties + if (headObject.hasOwnProperty(metaTemplateKeyName)) { + delete headObject[metaTemplateKeyName] + } + return false } diff --git a/test/unit/getMetaInfo.test.js b/test/unit/getMetaInfo.test.js index d80b69a6..d15bacb7 100644 --- a/test/unit/getMetaInfo.test.js +++ b/test/unit/getMetaInfo.test.js @@ -251,7 +251,7 @@ describe('getMetaInfo', () => { } }) - expect(getMetaInfo(component)).toEqual({ + const expectedMetaInfo = { title: 'Hello', titleChunk: 'Hello', titleTemplate: '%s', @@ -262,7 +262,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'Test title - My page' + content: 'Test title - My page', + template: true } ], base: [], @@ -272,7 +273,10 @@ describe('getMetaInfo', () => { noscript: [], __dangerouslyDisableSanitizers: [], __dangerouslyDisableSanitizersByTagID: {} - }) + } + + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) }) test('properly uses function meta templates', () => { @@ -290,7 +294,7 @@ describe('getMetaInfo', () => { } }) - expect(getMetaInfo(component)).toEqual({ + const expectedMetaInfo = { title: 'Hello', titleChunk: 'Hello', titleTemplate: '%s', @@ -301,7 +305,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'Test title - My page' + content: 'Test title - My page', + template: true } ], base: [], @@ -311,7 +316,10 @@ describe('getMetaInfo', () => { noscript: [], __dangerouslyDisableSanitizers: [], __dangerouslyDisableSanitizersByTagID: {} - }) + } + + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) }) test('properly uses content only if template is not defined', () => { @@ -460,7 +468,7 @@ describe('getMetaInfo', () => { render: h => h('div', null, [h('merge-child')]) }) - expect(getMetaInfo(component)).toEqual({ + const expectedMetaInfo = { title: 'Hello', titleChunk: 'Hello', titleTemplate: '%s', @@ -471,7 +479,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'An important title! - My page' + content: 'An important title! - My page', + template: true } ], base: [], @@ -481,7 +490,10 @@ describe('getMetaInfo', () => { noscript: [], __dangerouslyDisableSanitizers: [], __dangerouslyDisableSanitizersByTagID: {} - }) + } + + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) }) test('properly uses meta templates with one-level-deep nested children template', () => { @@ -514,7 +526,7 @@ describe('getMetaInfo', () => { render: h => h('div', null, [h('merge-child')]) }) - expect(getMetaInfo(component)).toEqual({ + const expectedMetaInfo = { title: 'Hello', titleChunk: 'Hello', titleTemplate: '%s', @@ -525,7 +537,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'Test title - My page' + content: 'Test title - My page', + template: true } ], base: [], @@ -535,7 +548,10 @@ describe('getMetaInfo', () => { noscript: [], __dangerouslyDisableSanitizers: [], __dangerouslyDisableSanitizersByTagID: {} - }) + } + + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) }) test('properly uses meta templates with one-level-deep nested children template and content', () => { @@ -569,7 +585,7 @@ describe('getMetaInfo', () => { render: h => h('div', null, [h('merge-child')]) }) - expect(getMetaInfo(component)).toEqual({ + const expectedMetaInfo = { title: 'Hello', titleChunk: 'Hello', titleTemplate: '%s', @@ -580,7 +596,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'An important title! - My page' + content: 'An important title! - My page', + template: true } ], base: [], @@ -590,7 +607,10 @@ describe('getMetaInfo', () => { noscript: [], __dangerouslyDisableSanitizers: [], __dangerouslyDisableSanitizersByTagID: {} - }) + } + + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) }) test('properly uses meta templates with one-level-deep nested children when parent has no template', () => { @@ -623,7 +643,7 @@ describe('getMetaInfo', () => { render: h => h('div', null, [h('merge-child')]) }) - expect(getMetaInfo(component)).toEqual({ + const expectedMetaInfo = { title: 'Hello', titleChunk: 'Hello', titleTemplate: '%s', @@ -634,7 +654,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'An important title! - My page' + content: 'An important title! - My page', + template: true } ], base: [], @@ -644,7 +665,10 @@ describe('getMetaInfo', () => { noscript: [], __dangerouslyDisableSanitizers: [], __dangerouslyDisableSanitizersByTagID: {} - }) + } + + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) + expect(getMetaInfo(component)).toEqual(expectedMetaInfo) }) test('no errors when metaInfo returns nothing', () => { @@ -721,7 +745,8 @@ describe('getMetaInfo', () => { { vmid: 'og:title', property: 'og:title', - content: 'Test title - My page' + content: 'Test title - My page', + template: true } ], base: [],