0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Added open graph tags for ghost head helper

issue #3900
- uses isPrivacyDisabled helper to see if useStructuredData has been disabled in config.js
- adds an array of promises to deal with asynchronous data
- resolves asynchronous data then adds open graph tags after canonical link
- featured image and tags are only added if present
- open graph tags only added on post and page
- adds unit test to check correct data is returned
- updates other unit tests to reflect changes
This commit is contained in:
cobbspur 2014-10-07 16:02:11 +01:00
parent 58ec6e0ac9
commit 487297ff81
4 changed files with 118 additions and 31 deletions

View file

@ -41,3 +41,10 @@ RPC pings only happen when Ghost is running in the `production` environment.
### Sharing Buttons ### Sharing Buttons
The default theme which comes with Ghost contains three sharing buttons to [Twitter](http://twitter.com), [Facebook](http://facebook.com), and [Google Plus](http://plus.google.com). No resources are loaded from any services, however the buttons do allow visitors to your blog to share your content publicly on these respective networks. The default theme which comes with Ghost contains three sharing buttons to [Twitter](http://twitter.com), [Facebook](http://facebook.com), and [Google Plus](http://plus.google.com). No resources are loaded from any services, however the buttons do allow visitors to your blog to share your content publicly on these respective networks.
### Structured Data
Ghost outputs Meta data for your blog that allows published content to be more easily machine-readable. This allows content to be easily discoverable in search engines as well as popular social networks where blog posts are typically shared.
This includes output for post.hbs in {{ghost_head}} based on the Open Graph protocol specification.

View file

@ -131,7 +131,7 @@ var PostSettingsMenuController = Ember.ObjectController.extend({
if (placeholder.length > 156) { if (placeholder.length > 156) {
// Limit to 156 characters // Limit to 156 characters
placeholder = placeholder.substring(0,156).trim(); placeholder = placeholder.substring(0, 156).trim();
placeholder = Ember.Handlebars.Utils.escapeExpression(placeholder); placeholder = Ember.Handlebars.Utils.escapeExpression(placeholder);
placeholder = new Ember.Handlebars.SafeString(placeholder + '…'); placeholder = new Ember.Handlebars.SafeString(placeholder + '…');
} }

View file

@ -491,20 +491,33 @@ coreHelpers.ghost_head = function (options) {
/*jshint unused:false*/ /*jshint unused:false*/
var self = this, var self = this,
blog = config.theme, blog = config.theme,
useStructuredData = !config.isPrivacyDisabled('useStructuredData'),
head = [], head = [],
majorMinor = /^(\d+\.)?(\d+)/, majorMinor = /^(\d+\.)?(\d+)/,
trimmedVersion = this.version, trimmedVersion = this.version,
trimmedUrlpattern = /.+(?=\/page\/\d*\/)/, trimmedUrlpattern = /.+(?=\/page\/\d*\/)/,
trimmedUrl, next, prev; trimmedUrl, next, prev, tags,
ops = [],
structuredData;
trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?'; trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?';
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />'); // Push Async calls to an array of promises
ops.push(coreHelpers.url.call(self, {hash: {absolute: true}}));
ops.push(coreHelpers.meta_description.call(self));
ops.push(coreHelpers.meta_title.call(self));
head.push('<link rel="alternate" type="application/rss+xml" title="' + // Resolves promises then push pushes meta data into ghost_head
_.escape(blog.title) + '" href="' + config.urlFor('rss') + '">'); return Promise.settle(ops).then(function (results) {
var url = results[0].value(),
metaDescription = results[1].value(),
metaTitle = results[2].value(),
publishedDate, modifiedDate;
if (!metaDescription) {
metaDescription = coreHelpers.excerpt.call(self.post, {hash: {words: '40'}});
}
return coreHelpers.url.call(self, {hash: {absolute: true}}).then(function (url) {
head.push('<link rel="canonical" href="' + url + '" />'); head.push('<link rel="canonical" href="' + url + '" />');
if (self.pagination) { if (self.pagination) {
@ -521,9 +534,44 @@ coreHelpers.ghost_head = function (options) {
} }
} }
// Test to see if we are on a post page and that Structured data has not been disabled in config.js
if (self.post && useStructuredData) {
publishedDate = moment(self.post.published_at).toISOString();
modifiedDate = moment(self.post.updated_at).toISOString();
structuredData = {
'og:site_name': _.escape(blog.title),
'og:type': 'article',
'og:title': metaTitle,
'og:description': metaDescription + '...',
'og:url': url,
'article:published_time': publishedDate,
'article:modified_time': modifiedDate
};
if (self.post.image) {
structuredData['og:image'] = _.escape(blog.url) + self.post.image;
}
_.each(structuredData, function (content, property) {
head.push('<meta property="' + property + '" content="' + content + '" />');
});
// Calls tag helper and assigns an array of tag names for a post
tags = coreHelpers.tags.call(self.post, {hash: {autolink: 'false'}}).string.split(',');
_.each(tags, function (tag) {
if (tag !== '') {
head.push('<meta property="article:tag" content="' + tag.trim() + '" />');
}
});
}
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="' +
_.escape(blog.title) + '" href="' + config.urlFor('rss') + '" />');
return filters.doFilter('ghost_head', head); return filters.doFilter('ghost_head', head);
}).then(function (head) { }).then(function (head) {
var headString = _.reduce(head, function (memo, item) { return memo + '\n' + item; }, ''); var headString = _.reduce(head, function (memo, item) { return memo + '\n ' + item; }, '');
return new hbs.handlebars.SafeString(headString.trim()); return new hbs.handlebars.SafeString(headString.trim());
}); });
}; };
@ -539,7 +587,7 @@ coreHelpers.ghost_foot = function (options) {
})); }));
return filters.doFilter('ghost_foot', foot).then(function (foot) { return filters.doFilter('ghost_foot', foot).then(function (foot) {
var footString = _.reduce(foot, function (memo, item) { return memo + ' ' + item; }, ''); var footString = _.reduce(foot, function (memo, item) { return memo + '\n' + item; }, '\n');
return new hbs.handlebars.SafeString(footString.trim()); return new hbs.handlebars.SafeString(footString.trim());
}); });
}; };

View file

@ -567,11 +567,11 @@ describe('Core Helpers', function () {
it('returns meta tag string', function (done) { it('returns meta tag string', function (done) {
config.set({url: 'http://testurl.com/'}); config.set({url: 'http://testurl.com/'});
helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) { helpers.ghost_head.call({version: '0.3.0', post: false}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n' + rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
'<link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/">\n' + ' <meta name="generator" content="Ghost 0.3" />\n' +
'<link rel="canonical" href="http://testurl.com/" />'); ' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done(); done();
}).catch(done); }).catch(done);
@ -581,9 +581,41 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com/'}); config.set({url: 'http://testurl.com/'});
helpers.ghost_head.call({version: '0.9'}).then(function (rendered) { helpers.ghost_head.call({version: '0.9'}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.9" />\n' + rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
'<link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/">\n' + ' <meta name="generator" content="Ghost 0.9" />\n' +
'<link rel="canonical" href="http://testurl.com/" />'); ' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done();
}).catch(done);
});
it('returns open graph data on post page', function (done) {
config.set({url: 'http://testurl.com/'});
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: '/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'}]
};
helpers.ghost_head.call({relativeUrl: '/post/', version: '0.3.0', post: post}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/post/" />\n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost" />\n' +
' <meta property="og:description" content="blog description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="og:image" content="http://testurl.com/test-image.png" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done(); done();
}).catch(done); }).catch(done);
@ -593,9 +625,9 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com/blog/'}); config.set({url: 'http://testurl.com/blog/'});
helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) { helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n' + rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/blog/" />\n' +
'<link rel="alternate" type="application/rss+xml" title="Ghost" href="/blog/rss/">\n' + ' <meta name="generator" content="Ghost 0.3" />\n' +
'<link rel="canonical" href="http://testurl.com/blog/" />'); ' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/blog/rss/" />');
done(); done();
}).catch(done); }).catch(done);
@ -605,9 +637,9 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com'}); config.set({url: 'http://testurl.com'});
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) { helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n' + rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/about/" />\n' +
'<link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/">\n' + ' <meta name="generator" content="Ghost 0.3" />\n' +
'<link rel="canonical" href="http://testurl.com/about/" />'); ' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done(); done();
}).catch(done); }).catch(done);
@ -617,11 +649,11 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com'}); config.set({url: 'http://testurl.com'});
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/page/3/', pagination: {next: '4', prev: '2'}}).then(function (rendered) { helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/page/3/', pagination: {next: '4', prev: '2'}}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n' + rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/page/3/" />\n' +
'<link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/">\n' + ' <link rel="prev" href="http://testurl.com/page/2/" />\n' +
'<link rel="canonical" href="http://testurl.com/page/3/" />\n' + ' <link rel="next" href="http://testurl.com/page/4/" />\n' +
'<link rel="prev" href="http://testurl.com/page/2/" />\n' + ' <meta name="generator" content="Ghost 0.3" />\n' +
'<link rel="next" href="http://testurl.com/page/4/" />'); ' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done(); done();
}).catch(done); }).catch(done);
}); });
@ -630,11 +662,11 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com'}); config.set({url: 'http://testurl.com'});
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/page/2/', pagination: {next: '3', prev: '1'}}).then(function (rendered) { helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/page/2/', pagination: {next: '3', prev: '1'}}).then(function (rendered) {
should.exist(rendered); should.exist(rendered);
rendered.string.should.equal('<meta name="generator" content="Ghost 0.3" />\n' + rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/page/2/" />\n' +
'<link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/">\n' + ' <link rel="prev" href="http://testurl.com/" />\n' +
'<link rel="canonical" href="http://testurl.com/page/2/" />\n' + ' <link rel="next" href="http://testurl.com/page/3/" />\n' +
'<link rel="prev" href="http://testurl.com/" />\n' + ' <meta name="generator" content="Ghost 0.3" />\n' +
'<link rel="next" href="http://testurl.com/page/3/" />'); ' <link rel="alternate" type="application/rss+xml" title="Ghost" href="/rss/" />');
done(); done();
}).catch(done); }).catch(done);
}); });