From 88eab9898caa457aba32911201ebfe7f9f60e788 Mon Sep 17 00:00:00 2001 From: Katharina Irrgang Date: Thu, 26 Oct 2017 12:03:53 +0200 Subject: [PATCH] Moved fetching client out of our `ghost_head` helper (#9180) refs #8995 - move the getClient lookup from ghost_head into middleware - use res.locals to keep track of the information (res.locals.client) - make the middleware global to all frontend routes - ghost_head: get locals from options.data not this (!) - adapt lot's of tests --- core/server/blog/app.js | 6 +- core/server/helpers/ghost_head.js | 81 +- core/server/middleware/frontend-client.js | 29 + core/test/unit/helpers/ghost_head_spec.js | 1096 ++++++++++++--------- core/test/utils/index.js | 27 + 5 files changed, 750 insertions(+), 489 deletions(-) create mode 100644 core/server/middleware/frontend-client.js diff --git a/core/server/blog/app.js b/core/server/blog/app.js index 457c4240d5..828d893cff 100644 --- a/core/server/blog/app.js +++ b/core/server/blog/app.js @@ -15,10 +15,11 @@ var debug = require('ghost-ignition').debug('blog'), // Global/shared middleware cacheControl = require('../middleware/cache-control'), - urlRedirects = require('../middleware/url-redirects'), errorHandler = require('../middleware/error-handler'), + frontendClient = require('../middleware/frontend-client'), maintenance = require('../middleware/maintenance'), prettyURLs = require('../middleware/pretty-urls'), + urlRedirects = require('../middleware/url-redirects'), // local middleware servePublicFile = require('../middleware/serve-public-file'), @@ -112,6 +113,9 @@ module.exports = function setupBlogApp() { // Blog frontend is cacheable blogApp.use(cacheControl('public')); + // Fetch the frontend client into res.locals + blogApp.use(frontendClient); + debug('General middleware done'); // Set up Frontend routes (including private blogging routes) diff --git a/core/server/helpers/ghost_head.js b/core/server/helpers/ghost_head.js index d0570216f1..61522ecf8d 100644 --- a/core/server/helpers/ghost_head.js +++ b/core/server/helpers/ghost_head.js @@ -8,7 +8,6 @@ var proxy = require('./proxy'), _ = require('lodash'), - Promise = require('bluebird'), debug = require('ghost-ignition').debug('ghost_head'), getMetaData = proxy.metaData.get, @@ -16,33 +15,11 @@ var proxy = require('./proxy'), escapeExpression = proxy.escapeExpression, SafeString = proxy.SafeString, filters = proxy.filters, - labs = proxy.labs, - api = proxy.api, logging = proxy.logging, settingsCache = proxy.settingsCache, config = proxy.config, blogIconUtils = proxy.blogIcon; -function getClient() { - if (labs.isSet('publicAPI') === true) { - return api.clients - .read({slug: 'ghost-frontend'}) - .then(function handleClient(client) { - client = client.clients[0]; - - if (client.status === 'enabled') { - return { - id: client.slug, - secret: client.secret - }; - } - - return {}; - }); - } - return Promise.resolve({}); -} - function writeMetaTag(property, content, type) { type = type || property.substring(0, 7) === 'twitter' ? 'name' : 'property'; return ''; @@ -82,27 +59,73 @@ function getAjaxHelper(clientId, clientSecret) { ''; } +/** + * **NOTE** + * Express adds `_locals`, see https://github.com/expressjs/express/blob/4.15.4/lib/response.js#L962. + * But `options.data.root.context` is available next to `root._locals.context`, because + * Express creates a `renderOptions` object, see https://github.com/expressjs/express/blob/4.15.4/lib/application.js#L554 + * and merges all locals to the root of the object. Very confusing, because the data is available in different layers. + * + * Express forwards the data like this to the hbs engine: + * { + * post: {}, - res.render('view', databaseResponse) + * context: ['post'], - from res.locals + * safeVersion: '1.x', - from res.locals + * _locals: { + * context: ['post'], + * safeVersion: '1.x' + * } + * } + * + * hbs forwards the data to any hbs helper like this + * { + * data: { + * blog: {}, + * labs: {}, + * config: {}, + * root: { + * post: {}, + * context: ['post'], + * locals: {...} + * } + * } + * + * `blog`, `labs` and `config` are the templateOptions, search for `hbs.updateTemplateOptions` in the code base. + * Also see how the root object get's created, https://github.com/wycats/handlebars.js/blob/v4.0.6/lib/handlebars/runtime.js#L259 + */ module.exports = function ghost_head(options) { debug('begin'); // if server error page do nothing - if (this.statusCode >= 500) { + if (options.data.root.statusCode >= 500) { return; } var head = [], + dataRoot = options.data.root, + context = dataRoot._locals.context ? dataRoot._locals.context : null, + client = dataRoot._locals.client, + safeVersion = dataRoot._locals.safeVersion, + postCodeInjection = dataRoot && dataRoot.post ? dataRoot.post.codeinjection_head : null, globalCodeinjection = settingsCache.get('ghost_head'), - postCodeInjection = options.data.root && options.data.root.post ? options.data.root.post.codeinjection_head : null, - context = this.context ? this.context : null, useStructuredData = !config.isPrivacyDisabled('useStructuredData'), - safeVersion = this.safeVersion, referrerPolicy = config.get('referrerPolicy') ? config.get('referrerPolicy') : 'no-referrer-when-downgrade', favicon = blogIconUtils.getIconUrl(), iconType = blogIconUtils.getIconType(favicon); debug('preparation complete, begin fetch'); - return Promise - .join(getMetaData(this, options.data.root), getClient(), function handleData(metaData, client) { + + /** + * @TODO: + * - getMetaData(dataRoot, dataRoot) -> yes that looks confusing! + * - there is a very mixed usage of `data.context` vs. `root.context` vs `root._locals.context` vs. `this.context` + * - NOTE: getMetaData won't live here anymore soon, see https://github.com/TryGhost/Ghost/issues/8995 + * - therefor we get rid of using `getMetaData(this, dataRoot)` + * - dataRoot has access to *ALL* locals, see function description + * - it should not break anything + */ + return getMetaData(dataRoot, dataRoot) + .then(function handleMetaData(metaData) { debug('end fetch'); if (context) { diff --git a/core/server/middleware/frontend-client.js b/core/server/middleware/frontend-client.js new file mode 100644 index 0000000000..1facca86a9 --- /dev/null +++ b/core/server/middleware/frontend-client.js @@ -0,0 +1,29 @@ +var api = require('../api'), + labs = require('../utils/labs'), + logging = require('../logging'); + +module.exports = function getFrontendClient(req, res, next) { + if (labs.isSet('publicAPI') !== true) { + return next(); + } + + return api.clients + .read({slug: 'ghost-frontend'}) + .then(function handleClient(client) { + client = client.clients[0]; + + if (client.status === 'enabled') { + res.locals.client = { + id: client.slug, + secret: client.secret + }; + } + + next(); + }) + .catch(function (err) { + // Log the error, but carry on as this is non-critical + logging.error(err); + next(); + }); +}; diff --git a/core/test/unit/helpers/ghost_head_spec.js b/core/test/unit/helpers/ghost_head_spec.js index 081c0cb1f2..030b0c4235 100644 --- a/core/test/unit/helpers/ghost_head_spec.js +++ b/core/test/unit/helpers/ghost_head_spec.js @@ -3,6 +3,7 @@ var should = require('should'), // jshint ignore:line _ = require('lodash'), Promise = require('bluebird'), moment = require('moment'), + testUtils = require('../../utils'), configUtils = require('../../utils/configUtils'), helpers = require('../../../server/helpers'), imageSize = require('../../../server/utils/image-size'), @@ -53,10 +54,14 @@ describe('{{ghost_head}} helper', function () { }); it('returns meta tag string on paginated index page without structured data and schema', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/page/2/', context: ['paged', 'index']}, - {data: {root: {context: ['paged', 'index']}}} - ).then(function (rendered) { + // @TODO: later we can extend this fn with an `meta` object e.g. locals.meta + helpers.ghost_head(testUtils.createHbsResponse({ + locals: { + relativeUrl: '/page/2/', + context: ['paged', 'index'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -71,10 +76,13 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data on first index page', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/', context: ['home', 'index']}, - {data: {root: {context: ['home', 'index']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + locals: { + relativeUrl: '/', + context: ['home', 'index'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -106,35 +114,41 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data on static page', function (done) { - var post = { - meta_description: 'all about our blog', - title: 'About', - feature_image: '/content/images/test-image-about.png', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - og_image: '', - og_title: '', - og_description: '', - twitter_image: '', - twitter_title: '', - twitter_description: '', - page: true, - author: { - name: 'Author name', - url: 'http://testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser', - bio: 'Author bio' + var renderObject = { + post: { + meta_description: 'all about our blog', + title: 'About', + feature_image: '/content/images/test-image-about.png', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + og_image: '', + og_title: '', + og_description: '', + twitter_image: '', + twitter_title: '', + twitter_description: '', + page: true, + author: { + name: 'Author name', + url: 'http://testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser', + bio: 'Author bio' + } } }; - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/about/', context: ['page'], post: post}, - {data: {root: {context: ['page']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/about/', + context: ['page'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -172,35 +186,41 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data on static page with custom post structured data', function (done) { - var post = { - meta_description: 'all about our blog', - title: 'About', - feature_image: '/content/images/test-image-about.png', - og_image: '/content/images/test-og-image.png', - og_title: 'Custom Facebook title', - og_description: 'Custom Facebook description', - twitter_image: '/content/images/test-twitter-image.png', - twitter_title: 'Custom Twitter title', - twitter_description: 'Custom Twitter description', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - page: true, - author: { - name: 'Author name', - url: 'http://testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser', - bio: 'Author bio' + var renderObject = { + post: { + meta_description: 'all about our blog', + title: 'About', + feature_image: '/content/images/test-image-about.png', + og_image: '/content/images/test-og-image.png', + og_title: 'Custom Facebook title', + og_description: 'Custom Facebook description', + twitter_image: '/content/images/test-twitter-image.png', + twitter_title: 'Custom Twitter title', + twitter_description: 'Custom Twitter description', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + page: true, + author: { + name: 'Author name', + url: 'http://testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser', + bio: 'Author bio' + } } }; - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/about/', context: ['page'], post: post}, - {data: {root: {context: ['page']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/about/', + context: ['page'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -238,17 +258,23 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data and schema first tag page with meta description and meta title', function (done) { - var tag = { - meta_description: 'tag meta description', - name: 'tagtitle', - meta_title: 'tag meta title', - feature_image: '/content/images/tag-image.png' + var renderObject = { + tag: { + meta_description: 'tag meta description', + name: 'tagtitle', + meta_title: 'tag meta title', + feature_image: '/content/images/tag-image.png' + } }; - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/tag/tagtitle/', tag: tag, context: ['tag']}, - {data: {root: {context: ['tag']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/tag/tagtitle/', + context: ['tag'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -280,18 +306,24 @@ describe('{{ghost_head}} helper', function () { }); it('tag first page without meta data if no meta title and meta description, but model description provided', function (done) { - var tag = { - meta_description: '', - description: 'tag description', - name: 'tagtitle', - meta_title: '', - feature_image: '/content/images/tag-image.png' + var renderObject = { + tag: { + meta_description: '', + description: 'tag description', + name: 'tagtitle', + meta_title: '', + feature_image: '/content/images/tag-image.png' + } }; - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/tag/tagtitle/', tag: tag, context: ['tag']}, - {data: {root: {context: ['tag']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/tag/tagtitle/', + context: ['tag'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -323,17 +355,23 @@ describe('{{ghost_head}} helper', function () { }); it('tag first page without meta and model description returns no description fields', function (done) { - var tag = { - meta_description: '', - name: 'tagtitle', - meta_title: '', - feature_image: '/content/images/tag-image.png' + var renderObject = { + tag: { + meta_description: '', + name: 'tagtitle', + meta_title: '', + feature_image: '/content/images/tag-image.png' + } }; - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/tag/tagtitle/', tag: tag, context: ['tag']}, - {data: {root: {context: ['tag']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/tag/tagtitle/', + context: ['tag'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.not.match(//); rendered.string.should.match(//); rendered.string.should.match(//); - rendered.string.should.match(//); + rendered.string.should.not.match(//); rendered.string.should.match(//); rendered.string.should.not.match(//); @@ -371,21 +415,27 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data and schema on first author page with cover image', function (done) { - var author = { - name: 'Author name', - slug: 'AuthorName', - bio: 'Author bio', - profile_image: '/content/images/test-author-image.png', - cover_image: '/content/images/author-cover-image.png', - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser' - }, authorBk = _.cloneDeep(author); + var renderObject = { + author: { + name: 'Author name', + slug: 'AuthorName', + bio: 'Author bio', + profile_image: '/content/images/test-author-image.png', + cover_image: '/content/images/author-cover-image.png', + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser' + } + }, authorBk = _.cloneDeep(renderObject.author); - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/author/AuthorName/', author: author, context: ['author']}, - {data: {root: {context: ['author']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/author/AuthorName/', + context: ['author'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -413,31 +463,32 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(/"name": "Author name"/); rendered.string.should.not.match(/"description":/); - author.should.eql(authorBk); + renderObject.author.should.eql(authorBk); done(); }).catch(done); }); it('does not return structured data on paginated author pages', function (done) { - var author = { - name: 'Author name', - slug: 'AuthorName', - bio: 'Author bio', - profile_image: '/content/images/test-author-image.png', - cover_image: '/content/images/author-cover-image.png', - website: 'http://authorwebsite.com' + var renderObject = { + author: { + name: 'Author name', + slug: 'AuthorName', + bio: 'Author bio', + profile_image: '/content/images/test-author-image.png', + cover_image: '/content/images/author-cover-image.png', + website: 'http://authorwebsite.com' + } }; - helpers.ghost_head.call( - { - safeVersion: '0.3', + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { relativeUrl: '/author/AuthorName/page/2/', - author: author, - context: ['paged', 'author'] - }, - {data: {root: {context: ['paged', 'author']}}} - ).then(function (rendered) { + context: ['paged', 'author'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -452,10 +503,12 @@ describe('{{ghost_head}} helper', function () { }); it('returns meta tag string even if safeVersion is invalid', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.9', context: []}, - {data: {root: {context: []}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + locals: { + context: [], + safeVersion: '0.9' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -466,40 +519,46 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data on post page with author image and post cover image', function (done) { - var post = { - meta_description: 'blog description', - custom_excerpt: '', - title: 'Welcome to Ghost', - feature_image: '/content/images/test-image.png', - og_image: '', - og_title: 'Custom Facebook title', - og_description: 'Custom Facebook description', - twitter_image: '/content/images/test-twitter-image.png', - twitter_title: '', - twitter_description: '', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], - author: { - name: 'Author name', - url: 'http://testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - bio: 'Author bio', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog description', + custom_excerpt: '', + title: 'Welcome to Ghost', + feature_image: '/content/images/test-image.png', + og_image: '', + og_title: 'Custom Facebook title', + og_description: 'Custom Facebook description', + twitter_image: '/content/images/test-twitter-image.png', + twitter_title: '', + twitter_description: '', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], + author: { + name: 'Author name', + url: 'http://testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + bio: 'Author bio', + facebook: 'testuser', + twitter: '@testuser' + } } - }, postBk = _.cloneDeep(post); + }, postBk = _.cloneDeep(renderObject.post); - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { - var re1 = new RegExp('/); @@ -546,47 +605,53 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(//); rendered.string.should.match(//); - post.should.eql(postBk); + renderObject.post.should.eql(postBk); done(); }).catch(done); }); it('returns structured data on post page with custom excerpt for description and meta description', function (done) { - var post = { - meta_description: 'blog description', - custom_excerpt: 'post custom excerpt', - title: 'Welcome to Ghost', - feature_image: '/content/images/test-image.png', - og_image: '/content/images/test-facebook-image.png', - og_title: '', - og_description: '', - twitter_image: '/content/images/test-twitter-image.png', - twitter_title: 'Custom Twitter title', - twitter_description: '', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], - author: { - name: 'Author name', - url: 'http://testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - bio: 'Author bio', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog description', + custom_excerpt: 'post custom excerpt', + title: 'Welcome to Ghost', + feature_image: '/content/images/test-image.png', + og_image: '/content/images/test-facebook-image.png', + og_title: '', + og_description: '', + twitter_image: '/content/images/test-twitter-image.png', + twitter_title: 'Custom Twitter title', + twitter_description: '', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], + author: { + name: 'Author name', + url: 'http://testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + bio: 'Author bio', + facebook: 'testuser', + twitter: '@testuser' + } } - }, postBk = _.cloneDeep(post); + }, postBk = _.cloneDeep(renderObject.post); - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { - var re1 = new RegExp('/); @@ -633,29 +698,35 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(//); rendered.string.should.match(//); - post.should.eql(postBk); + renderObject.post.should.eql(postBk); done(); }).catch(done); }); it('returns structured data on post page with fall back excerpt if no meta description provided', function (done) { - var post = { - meta_description: '', - custom_excerpt: '', - title: 'Welcome to Ghost', - html: '

This is a short post

', - author: { - name: 'Author name', - url: 'http://testauthorurl.com', - slug: 'Author' + var renderObject = { + post: { + meta_description: '', + custom_excerpt: '', + title: 'Welcome to Ghost', + html: '

This is a short post

', + author: { + name: 'Author name', + url: 'http://testauthorurl.com', + slug: 'Author' + } } - }, postBk = _.cloneDeep(post); + }, postBk = _.cloneDeep(renderObject.post); - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/post/', + context: ['post'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -686,46 +757,52 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(//); rendered.string.should.match(//); - post.should.eql(postBk); + renderObject.post.should.eql(postBk); done(); }).catch(done); }); it('returns structured data on AMP post page with author image and post cover image', function (done) { - var post = { - meta_description: 'blog description', - title: 'Welcome to Ghost', - feature_image: '/content/images/test-image.png', - og_image: '/content/images/test-facebook-image.png', - og_title: 'Custom Facebook title', - og_description: '', - twitter_image: '/content/images/test-twitter-image.png', - twitter_title: 'Custom Twitter title', - twitter_description: '', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], - author: { - name: 'Author name', - url: 'http://testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - bio: 'Author bio', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog description', + title: 'Welcome to Ghost', + feature_image: '/content/images/test-image.png', + og_image: '/content/images/test-facebook-image.png', + og_title: 'Custom Facebook title', + og_description: '', + twitter_image: '/content/images/test-twitter-image.png', + twitter_title: 'Custom Twitter title', + twitter_description: '', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], + author: { + name: 'Author name', + url: 'http://testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + bio: 'Author bio', + facebook: 'testuser', + twitter: '@testuser' + } } - }, postBk = _.cloneDeep(post); + }, postBk = _.cloneDeep(renderObject.post); - helpers.ghost_head.call( - {relativeUrl: '/post/amp/', safeVersion: '0.3', context: ['amp', 'post'], post: post}, - {data: {root: {context: ['amp', 'post']}}} - ).then(function (rendered) { - var re1 = new RegExp('/); @@ -772,40 +849,46 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(//); rendered.string.should.match(//); - post.should.eql(postBk); + renderObject.post.should.eql(postBk); done(); }).catch(done); }); it('returns structured data if metaTitle and metaDescription have double quotes', function (done) { - var post = { - meta_description: 'blog "test" description', - title: 'title', - meta_title: 'Welcome to Ghost "test"', - feature_image: '/content/images/test-image.png', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], - author: { - name: 'Author name', - url: 'http//:testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog "test" description', + title: 'title', + meta_title: 'Welcome to Ghost "test"', + feature_image: '/content/images/test-image.png', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], + author: { + name: 'Author name', + url: 'http//:testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser' + } } }; - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { - var re1 = new RegExp('/); @@ -859,31 +942,38 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data without tags if there are no tags', function (done) { - var post = { - meta_description: 'blog description', - title: 'Welcome to Ghost', - feature_image: '/content/images/test-image.png', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [], - author: { - name: 'Author name', - url: 'http//:testauthorurl.com', - slug: 'Author', - profile_image: '/content/images/test-author-image.png', - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog description', + title: 'Welcome to Ghost', + feature_image: '/content/images/test-image.png', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [], + author: { + name: 'Author name', + url: 'http//:testauthorurl.com', + slug: 'Author', + profile_image: '/content/images/test-author-image.png', + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser' + } } }; - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}}).then(function (rendered) { - var re1 = new RegExp('/); @@ -934,32 +1024,38 @@ describe('{{ghost_head}} helper', function () { }); it('returns structured data on post page with null author image and post cover image', function (done) { - var post = { - meta_description: 'blog description', - title: 'Welcome to Ghost', - feature_image: null, - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], - author: { - name: 'Author name', - url: 'http//:testauthorurl.com', - slug: 'Author', - profile_image: null, - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog description', + title: 'Welcome to Ghost', + feature_image: null, + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], + author: { + name: 'Author name', + url: 'http//:testauthorurl.com', + slug: 'Author', + profile_image: null, + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser' + } } }; - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { - var re1 = new RegExp('/); @@ -1010,10 +1106,13 @@ describe('{{ghost_head}} helper', function () { }); it('outputs structured data but not schema for custom channel', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/featured/', context: ['featured']}, - {data: {root: {context: ['featured']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + locals: { + relativeUrl: '/featured/', + context: ['featured'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1032,18 +1131,24 @@ describe('{{ghost_head}} helper', function () { }); it('returns twitter and facebook descriptions even if no meta description available', function (done) { - var post = { - title: 'Welcome to Ghost', - html: '

This is a short post

', - author: { - name: 'Author name' + var renderObject = { + post: { + title: 'Welcome to Ghost', + html: '

This is a short post

', + author: { + name: 'Author name' + } } }; - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/post/', + context: ['post'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.not.match(//); @@ -1056,18 +1161,24 @@ describe('{{ghost_head}} helper', function () { }); it('returns canonical URL', function (done) { - var post = { - title: 'Welcome to Ghost', - html: '

This is a short post

', - author: { - name: 'Author name' + var renderObject = { + post: { + title: 'Welcome to Ghost', + html: '

This is a short post

', + author: { + name: 'Author name' + } } }; - helpers.ghost_head.call( - {safeVersion: '0.3', relativeUrl: '/about/', context: ['page'], post: post}, - {data: {root: {context: ['page']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/about/', + context: ['page'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1079,15 +1190,16 @@ describe('{{ghost_head}} helper', function () { }); it('returns next & prev URL correctly for middle page', function (done) { - helpers.ghost_head.call( - { - safeVersion: '0.3', + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: { + pagination: {total: 4, page: 3, next: 4, prev: 2} + }, + locals: { relativeUrl: '/page/3/', context: ['paged', 'index'], - pagination: {next: '4', prev: '2'} - }, - {data: {root: {context: ['index', 'paged'], pagination: {total: 4, page: 3, next: 4, prev: 2}}}} - ).then(function (rendered) { + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1103,15 +1215,16 @@ describe('{{ghost_head}} helper', function () { }); it('returns next & prev URL correctly for second page', function (done) { - helpers.ghost_head.call( - { - safeVersion: '0.3', + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: { + pagination: {total: 3, page: 2, next: 3, prev: 1} + }, + locals: { relativeUrl: '/page/2/', context: ['paged', 'index'], - pagination: {next: '3', prev: '1'} - }, - {data: {root: {context: ['index', 'paged'], pagination: {total: 3, page: 2, next: 3, prev: 1}}}} - ).then(function (rendered) { + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1137,10 +1250,12 @@ describe('{{ghost_head}} helper', function () { }); it('returns correct rss url with subdirectory', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', context: ['paged', 'index']}, - {data: {root: {context: []}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + locals: { + context: ['paged', 'index'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1175,10 +1290,12 @@ describe('{{ghost_head}} helper', function () { }); it('contains the changed origin', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', context: ['paged', 'index']}, - {data: {root: {context: []}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + locals: { + context: ['paged', 'index'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1212,28 +1329,34 @@ describe('{{ghost_head}} helper', function () { }); it('does not return structured data', function (done) { - var post = { - meta_description: 'blog description', - title: 'Welcome to Ghost', - image: 'content/images/test-image.png', - published_at: moment('2008-05-31T19:18:15').toISOString(), - updated_at: moment('2014-10-06T15:23:54').toISOString(), - tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], - author: { - name: 'Author name', - url: 'http//:testauthorurl.com', - slug: 'Author', - image: 'content/images/test-author-image.png', - website: 'http://authorwebsite.com', - facebook: 'testuser', - twitter: '@testuser' + var renderObject = { + post: { + meta_description: 'blog description', + title: 'Welcome to Ghost', + image: 'content/images/test-image.png', + published_at: moment('2008-05-31T19:18:15').toISOString(), + updated_at: moment('2014-10-06T15:23:54').toISOString(), + tags: [{name: 'tag1'}, {name: 'tag2'}, {name: 'tag3'}], + author: { + name: 'Author name', + url: 'http//:testauthorurl.com', + slug: 'Author', + image: 'content/images/test-author-image.png', + website: 'http://authorwebsite.com', + facebook: 'testuser', + twitter: '@testuser' + } } }; - helpers.ghost_head.call( - {relativeUrl: '/post/', safeVersion: '0.3', context: ['post'], post: post}, - {data: {root: {context: ['post']}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: renderObject, + locals: { + relativeUrl: '/post/', + context: ['post'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1269,10 +1392,15 @@ describe('{{ghost_head}} helper', function () { }); it('returns meta tag plus injected code', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', context: ['paged', 'index'], post: false}, - {data: {root: {context: []}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: { + post: false + }, + locals: { + context: ['paged', 'index'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(//); rendered.string.should.match(//); @@ -1288,10 +1416,17 @@ describe('{{ghost_head}} helper', function () { }); it('outputs post codeinjection as well', function (done) { - helpers.ghost_head.call( - {safeVersion: '0.3', context: ['paged', 'index']}, - {data: {root: {context: [], post: {codeinjection_head: 'post-codeinjection'}}}} - ).then(function (rendered) { + helpers.ghost_head(testUtils.createHbsResponse({ + renderObject: { + post: { + codeinjection_head: 'post-codeinjection' + } + }, + locals: { + context: ['paged', 'index'], + safeVersion: '0.3' + } + })).then(function (rendered) { should.exist(rendered); rendered.string.should.match(/