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); });