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: ``,
+ 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
@@ -85,6 +133,10 @@ export default function createApp () {
{{ user.id }}: {{ user.name }}
+
+
`
})
+
+ 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: [],