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\/"/);