diff --git a/core/server/data/meta/description.js b/core/server/data/meta/description.js index 1900162ef6..dcffe7f0c1 100644 --- a/core/server/data/meta/description.js +++ b/core/server/data/meta/description.js @@ -12,9 +12,9 @@ function getDescription(data, root) { } else if (_.contains(context, 'home')) { description = config.theme.description; } else if (_.contains(context, 'author') && data.author) { - description = data.author.bio; + description = data.author.meta_description || data.author.bio; } else if (_.contains(context, 'tag') && data.tag) { - description = data.tag.meta_description; + description = data.tag.meta_description || data.tag.description; } else if ((_.contains(context, 'post') || _.contains(context, 'page')) && data.post) { description = data.post.meta_description; } diff --git a/core/server/helpers/ghost_head.js b/core/server/helpers/ghost_head.js index f5e76bbcfe..f73bdc44fc 100644 --- a/core/server/helpers/ghost_head.js +++ b/core/server/helpers/ghost_head.js @@ -6,26 +6,18 @@ // We use the name ghost_head to match the helper for consistency: // jscs:disable requireCamelCaseOrUpperCaseIdentifiers -var hbs = require('express-hbs'), - moment = require('moment'), - _ = require('lodash'), - Promise = require('bluebird'), - - config = require('../config'), - filters = require('../filters'), - - api = require('../api'), - assetHelper = require('./asset'), - urlHelper = require('./url'), - meta_description = require('./meta_description'), - meta_title = require('./meta_title'), - excerpt = require('./excerpt'), - tagsHelper = require('./tags'), - imageHelper = require('./image'), - labs = require('../utils/labs'), - - blog, - ghost_head; +var getMetaData = require('../data/meta'), + hbs = require('express-hbs'), + escapeExpression = hbs.handlebars.Utils.escapeExpression, + SafeString = hbs.handlebars.SafeString, + _ = require('lodash'), + api = require('../api'), + filters = require('../filters'), + assetHelper = require('./asset'), + config = require('../config'), + Promise = require('bluebird'), + labs = require('../utils/labs'), + api = require('../api'); function getClient() { if (labs.isSet('publicAPI') === true) { @@ -37,12 +29,10 @@ function getClient() { secret: client.secret }; } - return {}; }); } - - return {}; + return Promise.resolve({}); } function writeMetaTag(property, content, type) { @@ -50,234 +40,26 @@ function writeMetaTag(property, content, type) { return ''; } -function getImage(props, context, contextObject) { - if (context === 'home' || context === 'author') { - contextObject.image = contextObject.cover; - } - - props.image = imageHelper.call(contextObject, {hash: {absolute: true}}); - - if (context === 'post' && contextObject.author) { - props.author_image = imageHelper.call(contextObject.author, {hash: {absolute: true}}); - } -} - -function getPaginationUrls(pagination, relativeUrl, secure, head) { - var trimmedUrl, next, prev, - trimmedUrlpattern = /.+(?=\/page\/\d*\/)/, - tagOrAuthorPattern = /\/(tag)|(author)\//; - - trimmedUrl = relativeUrl.match(trimmedUrlpattern); - if (pagination.prev) { - prev = (pagination.prev > 1 ? '/page/' + pagination.prev + '/' : '/'); - prev = (trimmedUrl) ? trimmedUrl + prev : prev; - head.push('' - ); - } - if (pagination.next) { - next = '/page/' + pagination.next + '/'; - if (trimmedUrl) { - next = trimmedUrl + next; - } else if (tagOrAuthorPattern.test(relativeUrl)) { - next = relativeUrl.slice(0, -1) + next; - } - head.push('' - ); - } - return head; -} - -function addContextMetaData(context, data, metaData) { - // escaped data - metaData.metaTitle = hbs.handlebars.Utils.escapeExpression(metaData.metaTitle); - metaData.metaDescription = metaData.metaDescription ? hbs.handlebars.Utils.escapeExpression(metaData.metaDescription) : null; - - if (context === 'author') { - metaData.authorUrl = hbs.handlebars.Utils.escapeExpression(blog.url + '/author/' + data.author.slug); - metaData.ogType = 'profile'; - } else if (context === 'post') { - metaData.publishedDate = moment(data.post.published_at).toISOString(); - metaData.modifiedDate = moment(data.post.updated_at).toISOString(); - metaData.authorUrl = hbs.handlebars.Utils.escapeExpression(blog.url + '/author/' + data.post.author.slug); - metaData.ogType = 'article'; - } - return metaData; -} - -function initMetaData(context, data, results) { - var metaData = { - url: results.url, - canonicalUrl: results.canonicalUrl, - metaDescription: results.meta_description || null, - metaTitle: results.meta_title, - coverImage: results.image, - authorImage: results.author_image, - publishedDate: null, - modifiedDate: null, - tags: null, - card: 'summary', - authorUrl: null, - ogType: 'website', - keywords: null, - blog: blog, - title: blog.title, - clientId: results.client.id, - clientSecret: results.client.secret - }; - - if (!metaData.metaDescription) { - if (context === 'post') { - metaData.metaDescription = excerpt.call(data.post, {hash: {words: '40'}}).string + '...'; - } else if (context === 'tag') { - metaData.metaDescription = data.tag.description ? data.tag.description : null; - } - } - return addContextMetaData(context, data, metaData); -} - -function getStructuredData(metaData) { - var structuredData; - - if (metaData.coverImage) { - metaData.card = 'summary_large_image'; - } - - structuredData = { - 'og:site_name': metaData.title, - 'og:type': metaData.ogType, - 'og:title': metaData.metaTitle, - 'og:description': metaData.metaDescription, - 'og:url': metaData.canonicalUrl, - 'og:image': metaData.coverImage, - 'article:published_time': metaData.publishedDate, - 'article:modified_time': metaData.modifiedDate, - 'article:tag': metaData.tags, - 'twitter:card': metaData.card, - 'twitter:title': metaData.metaTitle, - 'twitter:description': metaData.metaDescription, - 'twitter:url': metaData.canonicalUrl, - 'twitter:image:src': metaData.coverImage - }; - - return structuredData; -} - -// Creates the final schema object with values that are not null -function trimSchema(schema) { - var schemaObject = {}; - - _.each(schema, function (value, key) { - if (value !== null && value !== undefined) { - schemaObject[key] = value; - } - }); - return schemaObject; -} - -function getPostSchema(metaData, data) { - var schema = { - '@context': 'http://schema.org', - '@type': 'Article', - publisher: metaData.title, - author: { - '@type': 'Person', - name: data.post.author.name, - image: metaData.authorImage, - url: metaData.authorUrl, - sameAs: data.post.author.website || null, - description: data.post.author.bio || null - }, - headline: metaData.metaTitle, - url: metaData.url, - datePublished: metaData.publishedDate, - dateModified: metaData.modifiedDate, - image: metaData.coverImage, - keywords: metaData.keywords, - description: metaData.metaDescription - }; - return trimSchema(schema); -} - -function getTagSchema(metaData, data) { - var schema = { - '@context': 'http://schema.org', - '@type': 'Series', - publisher: metaData.title, - url: metaData.url, - image: metaData.coverImage, - name: data.tag.name, - description: metaData.metaDescription - }; - - return trimSchema(schema); -} - -function getAuthorSchema(metaData, data) { - var schema = { - '@context': 'http://schema.org', - '@type': 'Person', - sameAs: data.author.website || null, - publisher: metaData.title, - name: data.author.name, - url: metaData.authorUrl, - image: metaData.coverImage, - description: metaData.metaDescription - }; - - return trimSchema(schema); -} - -function getHomeSchema(metaData) { - var schema = { - '@context': 'http://schema.org', - '@type': 'Website', - publisher: metaData.title, - url: metaData.url, - image: metaData.coverImage, - description: metaData.metaDescription - }; - - return trimSchema(schema); -} - -function chooseSchema(metaData, context, data) { - if (context === 'post') { - return getPostSchema(metaData, data); - } else if (context === 'home') { - return getHomeSchema(metaData); - } else if (context === 'tag') { - return getTagSchema(metaData, data); - } else if (context === 'author') { - return getAuthorSchema(metaData, data); - } -} - -function finaliseStructuredData(structuredData, tags, head) { - _.each(structuredData, function (content, property) { +function finaliseStructuredData(metaData) { + var head = []; + _.each(metaData.structuredData, function (content, property) { if (property === 'article:tag') { - _.each(tags, function (tag) { - if (tag !== '') { - tag = hbs.handlebars.Utils.escapeExpression(tag.trim()); - head.push(writeMetaTag(property, tag)); + _.each(metaData.keywords, function (keyword) { + if (keyword !== '') { + keyword = escapeExpression(keyword); + head.push(writeMetaTag(property, + escapeExpression(keyword))); } }); head.push(''); } else if (content !== null && content !== undefined) { - head.push(writeMetaTag(property, content)); + head.push(writeMetaTag(property, + escapeExpression(content))); } }); return head; } -function finaliseSchema(schema, head) { - head.push('\n' - ); - return head; -} - function getAjaxHelper(clientId, clientSecret) { return '\n' + @@ -289,90 +71,62 @@ function getAjaxHelper(clientId, clientSecret) { ''; } -ghost_head = function (options) { +function ghost_head(options) { // if error page do nothing if (this.code >= 400) { return; } - - // create a shortcut for theme config - blog = config.theme; - - /*jshint unused:false*/ - var self = this, - useStructuredData = !config.isPrivacyDisabled('useStructuredData'), + var metaData = getMetaData(this, options.data.root), head = [], - safeVersion = this.safeVersion, - props = {}, - structuredData, - schema, - title = hbs.handlebars.Utils.escapeExpression(blog.title), - context = self.context ? self.context[0] : null, - contextObject = _.cloneDeep(self[context] || blog); + context = this.context ? this.context[0] : null, + useStructuredData = !config.isPrivacyDisabled('useStructuredData'), + safeVersion = this.safeVersion; - // Store Async calls in an object of named promises - props.url = urlHelper.call(self, {hash: {absolute: true}}); - props.canonicalUrl = config.urlJoin(config.getBaseUrl(false), - urlHelper.call(self, {hash: {absolute: false}})); - props.meta_description = meta_description.call(self, options); - props.meta_title = meta_title.call(self, options); - props.client = getClient(); - getImage(props, context, contextObject); - - // Resolves promises then push pushes meta data into ghost_head - return Promise.props(props).then(function (results) { + return getClient().then(function (client) { if (context) { - var metaData = initMetaData(context, self, results), - tags = tagsHelper.call(self.post, {hash: {autolink: 'false'}}).string.split(','); - - // If there are tags - build the keywords metaData string - if (tags[0] !== '') { - metaData.keywords = hbs.handlebars.Utils.escapeExpression(tagsHelper.call(self.post, - {hash: {autolink: 'false', separator: ', '}} - ).string); - } - // head is our main array that holds our meta data - head.push(''); + head.push(''); head.push(''); - // Generate context driven pagination urls - if (self.pagination) { - getPaginationUrls(self.pagination, self.relativeUrl, self.secure, head); + if (metaData.previousUrl) { + head.push(''); + } + + if (metaData.nextUrl) { + head.push(''); } - // Test to see if we are on a post page and that Structured data has not been disabled in config.js if (context !== 'paged' && context !== 'page' && useStructuredData) { - // Create context driven OpenGraph and Twitter meta data - structuredData = getStructuredData(metaData); - // Create context driven JSONLD object - schema = chooseSchema(metaData, context, self); head.push(''); - // Formats structured data and pushes to head array - finaliseStructuredData(structuredData, tags, head); + head.push.apply(head, finaliseStructuredData(metaData)); head.push(''); - // Formats schema script/JSONLD data and pushes to head array - finaliseSchema(schema, head); + + head.push('\n'); } - if (metaData.clientId && metaData.clientSecret) { - head.push(getAjaxHelper(metaData.clientId, metaData.clientSecret)); + if (client && client.id && client.secret) { + head.push(getAjaxHelper(client.id, client.secret)); } } - head.push(''); + head.push(''); head.push(''); - }).then(function () { + escapeExpression(metaData.blog.title) + '" href="' + + escapeExpression(metaData.rssUrl) + '" />'); + return api.settings.read({key: 'ghost_head'}); }).then(function (response) { head.push(response.settings[0].value); return filters.doFilter('ghost_head', head); }).then(function (head) { - var headString = _.reduce(head, function (memo, item) { return memo + '\n ' + item; }, ''); - return new hbs.handlebars.SafeString(headString.trim()); + return new SafeString(head.join('\n ').trim()); }); -}; +} module.exports = ghost_head; diff --git a/core/test/unit/metadata/description_spec.js b/core/test/unit/metadata/description_spec.js index 048b580978..43a1061eaf 100644 --- a/core/test/unit/metadata/description_spec.js +++ b/core/test/unit/metadata/description_spec.js @@ -38,6 +38,18 @@ describe('getMetaDescription', function () { description.should.equal('Best tag ever!'); }); + it('should return data tag description if no meta description for tag', function () { + var description = getMetaDescription({ + tag: { + meta_description: '', + description: 'The normal description' + } + }, { + context: ['tag'] + }); + description.should.equal('The normal description'); + }); + it('should return data post meta description if on root context contains post', function () { var description = getMetaDescription({ post: { diff --git a/core/test/unit/server_helpers/ghost_head_spec.js b/core/test/unit/server_helpers/ghost_head_spec.js index b19dae5fca..0f98aba55b 100644 --- a/core/test/unit/server_helpers/ghost_head_spec.js +++ b/core/test/unit/server_helpers/ghost_head_spec.js @@ -269,7 +269,7 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(/"@type": "Person"/); rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/); rendered.string.should.match(/"publisher": "Ghost"/); - rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/AuthorName"/); + rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/AuthorName\/"/); rendered.string.should.match(/"image": "http:\/\/testurl.com\/content\/images\/author-cover-image.png"/); rendered.string.should.match(/"name": "Author name"/); rendered.string.should.match(/"description": "Author bio"/); @@ -371,7 +371,7 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(/"@type": "Person"/); rendered.string.should.match(/"name": "Author name"/); rendered.string.should.match(/"image\": \"http:\/\/testurl.com\/content\/images\/test-author-image.png\"/); - rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/); + rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author\/"/); rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/); rendered.string.should.match(/"description": "Author bio"/); rendered.string.should.match(/"headline": "Welcome to Ghost"/); @@ -444,7 +444,7 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(/"@type": "Person"/); rendered.string.should.match(/"name": "Author name"/); rendered.string.should.match(/"image\": \"http:\/\/testurl.com\/content\/images\/test-author-image.png\"/); - rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/); + rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author\/"/); rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/); rendered.string.should.match(/"headline": "Welcome to Ghost "test""/); rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/); @@ -511,7 +511,7 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(/"@type": "Person"/); rendered.string.should.match(/"name": "Author name"/); rendered.string.should.match(/"image\": \"http:\/\/testurl.com\/content\/images\/test-author-image.png\"/); - rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/); + rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author\/"/); rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/); rendered.string.should.match(/"headline": "Welcome to Ghost"/); rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/); @@ -581,7 +581,7 @@ describe('{{ghost_head}} helper', function () { rendered.string.should.match(/"@type": "Person"/); rendered.string.should.match(/"name": "Author name"/); rendered.string.should.not.match(/"image\"/); - rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author"/); + rendered.string.should.match(/"url": "http:\/\/testurl.com\/author\/Author\/"/); rendered.string.should.match(/"sameAs": "http:\/\/authorwebsite.com"/); rendered.string.should.match(/"headline": "Welcome to Ghost"/); rendered.string.should.match(/"url": "http:\/\/testurl.com\/post\/"/);