diff --git a/PRIVACY.md b/PRIVACY.md
index d327d01584..45868e5368 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -41,3 +41,10 @@ RPC pings only happen when Ghost is running in the `production` environment.
### 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.
+
+
+### 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.
\ No newline at end of file
diff --git a/core/client/controllers/post-settings-menu.js b/core/client/controllers/post-settings-menu.js
index 7e8762308b..d566791588 100644
--- a/core/client/controllers/post-settings-menu.js
+++ b/core/client/controllers/post-settings-menu.js
@@ -131,7 +131,7 @@ var PostSettingsMenuController = Ember.ObjectController.extend({
if (placeholder.length > 156) {
// Limit to 156 characters
- placeholder = placeholder.substring(0,156).trim();
+ placeholder = placeholder.substring(0, 156).trim();
placeholder = Ember.Handlebars.Utils.escapeExpression(placeholder);
placeholder = new Ember.Handlebars.SafeString(placeholder + '…');
}
diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js
index e9a303603f..4987435271 100644
--- a/core/server/helpers/index.js
+++ b/core/server/helpers/index.js
@@ -491,20 +491,33 @@ coreHelpers.ghost_head = function (options) {
/*jshint unused:false*/
var self = this,
blog = config.theme,
+ useStructuredData = !config.isPrivacyDisabled('useStructuredData'),
head = [],
majorMinor = /^(\d+\.)?(\d+)/,
trimmedVersion = this.version,
trimmedUrlpattern = /.+(?=\/page\/\d*\/)/,
- trimmedUrl, next, prev;
+ trimmedUrl, next, prev, tags,
+ ops = [],
+ structuredData;
trimmedVersion = trimmedVersion ? trimmedVersion.match(majorMinor)[0] : '?';
- head.push('');
+ // 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('');
+ // Resolves promises then push pushes meta data into ghost_head
+ 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('');
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('');
+ });
+
+ // 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('');
+ }
+ });
+ }
+ head.push('');
+ head.push('');
return filters.doFilter('ghost_head', 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());
});
};
@@ -539,7 +587,7 @@ coreHelpers.ghost_foot = function (options) {
}));
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());
});
};
diff --git a/core/test/unit/server_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js
index e6eb3e3205..9d0ef9ef59 100644
--- a/core/test/unit/server_helpers_index_spec.js
+++ b/core/test/unit/server_helpers_index_spec.js
@@ -567,11 +567,11 @@ describe('Core Helpers', function () {
it('returns meta tag string', function (done) {
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);
- rendered.string.should.equal('\n' +
- '\n' +
- '');
+ rendered.string.should.equal('\n' +
+ ' \n' +
+ ' ');
done();
}).catch(done);
@@ -581,9 +581,41 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com/'});
helpers.ghost_head.call({version: '0.9'}).then(function (rendered) {
should.exist(rendered);
- rendered.string.should.equal('\n' +
- '\n' +
- '');
+ rendered.string.should.equal('\n' +
+ ' \n' +
+ ' ');
+
+ 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('\n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' ');
done();
}).catch(done);
@@ -593,9 +625,9 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com/blog/'});
helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) {
should.exist(rendered);
- rendered.string.should.equal('\n' +
- '\n' +
- '');
+ rendered.string.should.equal('\n' +
+ ' \n' +
+ ' ');
done();
}).catch(done);
@@ -605,9 +637,9 @@ describe('Core Helpers', function () {
config.set({url: 'http://testurl.com'});
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) {
should.exist(rendered);
- rendered.string.should.equal('\n' +
- '\n' +
- '');
+ rendered.string.should.equal('\n' +
+ ' \n' +
+ ' ');
done();
}).catch(done);
@@ -617,11 +649,11 @@ describe('Core Helpers', function () {
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) {
should.exist(rendered);
- rendered.string.should.equal('\n' +
- '\n' +
- '\n' +
- '\n' +
- '');
+ rendered.string.should.equal('\n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' ');
done();
}).catch(done);
});
@@ -630,11 +662,11 @@ describe('Core Helpers', function () {
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) {
should.exist(rendered);
- rendered.string.should.equal('\n' +
- '\n' +
- '\n' +
- '\n' +
- '');
+ rendered.string.should.equal('\n' +
+ ' \n' +
+ ' \n' +
+ ' \n' +
+ ' ');
done();
}).catch(done);
});