0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Merge pull request #4578 from ErisDS/pr-3489

Code Injection PR 3489 rebased and updated
This commit is contained in:
Sebastian Gierlinger 2014-12-04 15:16:37 +01:00
commit 32959b2269
15 changed files with 589 additions and 333 deletions

View file

@ -388,6 +388,9 @@ $i-compass: \e602;
.icon-lightning:before {
content: '#{$i-lightning}';
}
.icon-code:before {
content: '#{$i-code}';
}
.icon-atom:before {
content: '#{$i-atom}';
}

View file

@ -1,6 +1,25 @@
var SettingsController = Ember.Controller.extend({
showApps: Ember.computed.bool('config.apps'),
showTags: Ember.computed.bool('config.tagsUI')
showGeneral: Ember.computed('session.user.name', function () {
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') ? false : true;
}),
showUsers: Ember.computed('session.user.name', function () {
return this.get('session.user.isAuthor') ? false : true;
}),
showTags: Ember.computed('session.user.name', 'config.tagsUI', function () {
return this.get('session.user.isAuthor') || !this.get('config.tagsUI') ? false : true;
}),
showCodeInjection: Ember.computed('session.user.name', 'config.codeInjectionUI', function () {
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') || !this.get('config.codeInjectionUI') ? false : true;
}),
showLabs: Ember.computed('session.user.name', function () {
return this.get('session.user.isAuthor') || this.get('session.user.isEditor') ? false : true;
}),
showAbout: Ember.computed('session.user.name', function () {
return this.get('session.user.isAuthor') ? false : true;
})
});
export default SettingsController;

View file

@ -0,0 +1,19 @@
var SettingsCodeInjectionController = Ember.ObjectController.extend({
actions: {
save: function () {
var self = this;
return this.get('model').save().then(function (model) {
self.notifications.closePassive();
self.notifications.showSuccess('Settings successfully saved.');
return model;
}).catch(function (errors) {
self.notifications.closePassive();
self.notifications.showErrors(errors);
});
}
}
});
export default SettingsCodeInjectionController;

View file

@ -14,7 +14,9 @@ var Setting = DS.Model.extend(NProgressSaveMixin, ValidationEngine, {
forceI18n: DS.attr('boolean'),
permalinks: DS.attr('string'),
activeTheme: DS.attr('string'),
availableThemes: DS.attr()
availableThemes: DS.attr(),
ghost_head: DS.attr('string'),
ghost_foot: DS.attr('string')
});
export default Setting;

View file

@ -40,6 +40,7 @@ Router.map(function () {
this.route('about');
this.route('tags');
this.route('labs');
this.route('code-injection');
});
// Redirect debug to settings labs

View file

@ -0,0 +1,37 @@
import AuthenticatedRoute from 'ghost/routes/authenticated';
import loadingIndicator from 'ghost/mixins/loading-indicator';
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
import styleBody from 'ghost/mixins/style-body';
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
import ctrlOrCmd from 'ghost/utils/ctrl-or-cmd';
var shortcuts = {},
SettingsCodeInjectionRoute;
shortcuts[ctrlOrCmd + '+s'] = {action: 'save'};
SettingsCodeInjectionRoute = AuthenticatedRoute.extend(styleBody, loadingIndicator, CurrentUserSettings, ShortcutsRoute, {
classNames: ['settings-view-code'],
beforeModel: function () {
return this.currentUser()
.then(this.transitionAuthor())
.then(this.transitionEditor());
},
model: function () {
return this.store.find('setting', {type: 'blog,theme'}).then(function (records) {
return records.get('firstObject');
});
},
shortcuts: shortcuts,
actions: {
save: function () {
this.get('controller').send('save');
}
}
});
export default SettingsCodeInjectionRoute;

View file

@ -6,22 +6,30 @@
<div class="page-content">
<nav class="settings-nav js-settings-menu">
<ul>
{{#unless session.user.isAuthor}}
{{#unless session.user.isEditor}}
{{gh-activating-list-item route="settings.general" title="General" classNames="settings-nav-general icon-settings"}}
{{/unless}}
{{! Whilst tag management is still in development only show tags button if there if tagsUI is true in config.js --}}
{{#if showTags}}
{{gh-activating-list-item route="settings.tags" title="Tags" classNames="settings-nav-tags icon-tag"}}
{{/if}}
{{#if showGeneral}}
{{gh-activating-list-item route="settings.general" title="General" classNames="settings-nav-general icon-settings"}}
{{/if}}
{{#if showUsers}}
{{gh-activating-list-item route="settings.users" title="Users" classNames="settings-nav-users icon-users"}}
{{/if}}
{{#if showTags}}
{{gh-activating-list-item route="settings.tags" title="Tags" classNames="settings-nav-tags icon-tag"}}
{{/if}}
{{#if showCodeInjection}}
{{gh-activating-list-item route="settings.code-injection" title="Code Injection" classNames="settings-nav-code icon-code"}}
{{/if}}
{{#if showLabs}}
{{gh-activating-list-item route="settings.labs" title="Labs" classNames="settings-nav-labs icon-atom"}}
{{/unless}}
{{/if}}
{{gh-activating-list-item route="settings.about" title="About" classNames="settings-nav-about icon-pacman"}}
{{#if showAbout}}
{{gh-activating-list-item route="settings.about" title="About" classNames="settings-nav-about icon-pacman"}}
{{/if}}
</ul>
</nav>

View file

@ -0,0 +1,31 @@
<header class="settings-view-header">
{{#link-to "settings" class="btn btn-default btn-back"}}Back{{/link-to}}
<h2 class="page-title">Code Injection</h2>
<section class="page-actions">
<button type="button" class="btn btn-blue" {{action "save"}}>Save</button>
</section>
</header>
<section class="content settings-code">
<form id="settings-code" novalidate="novalidate">
<fieldset>
<div class="form-group">
<p>
Ghost allows you to inject code into the top and bottom of your template files without editing them. This allows for quick modifications to insert useful things like tracking codes and meta data.
</p>
</div>
<div class="form-group">
<label for="blog-header">Blog Header</label>
<p>Code here will be injected to the \{{ghost_head}} helper at the top of your page</p>
{{textarea id="ghost-head" name="codeInjection[ghost_head]" type="text" value=ghost_head}}
</div>
<div class="form-group">
<label for="blog-header">Blog Footer</label>
<p>Code here will be injected to the \{{ghost_foot}} helper at the bottom of your page</p>
{{textarea id="blog-foot" name="codeInjection[ghost_foot]" type="text" value=ghost_foot}}
</div>
</fieldset>
</form>
</section>

View file

@ -0,0 +1,5 @@
import BaseView from 'ghost/views/settings/content-base';
var SettingsGeneralView = BaseView.extend();
export default SettingsGeneralView;

View file

@ -12,6 +12,7 @@ function getValidKeys() {
fileStorage: config.fileStorage === false ? false : true,
apps: config.apps === true ? true : false,
tagsUI: config.tagsUI === true ? true : false,
codeInjectionUI: config.codeInjectionUI === true ? true : false,
version: config.ghostVersion,
environment: process.env.NODE_ENV,
database: config.database.client,

View file

@ -61,6 +61,12 @@
"matches": "(:id|:slug|:year|:month|:day)",
"notContains": "/ghost/"
}
},
"ghost_head": {
"defaultValue" : ""
},
"ghost_foot": {
"defaultValue" : ""
}
},
"theme": {

View file

@ -10,6 +10,7 @@ var hbs = require('express-hbs'),
_ = require('lodash'),
config = require('../config'),
filters = require('../filters'),
api = require('../api'),
utils = require('./utils'),
ghost_foot;
@ -23,8 +24,13 @@ ghost_foot = function (options) {
version: config.assetHash
}));
return filters.doFilter('ghost_foot', foot).then(function (foot) {
var footString = _.reduce(foot, function (memo, item) { return memo + '\n' + item; }, '\n');
return api.settings.read({key: 'ghost_foot'}).then(function (response) {
foot.push(response.settings[0].value);
return foot;
}).then(function (foot) {
return filters.doFilter('ghost_foot', foot);
}).then(function (foot) {
var footString = _.reduce(foot, function (memo, item) { return memo + ' ' + item; }, '');
return new hbs.handlebars.SafeString(footString.trim());
});
};

View file

@ -14,6 +14,7 @@ var hbs = require('express-hbs'),
config = require('../config'),
filters = require('../filters'),
api = require('../api'),
urlHelper = require('./url'),
meta_description = require('./meta_description'),
meta_title = require('./meta_title'),
@ -146,7 +147,7 @@ ghost_head = function (options) {
});
head.push('');
} else if (content !== null && content !== undefined) {
type = property.substring(0, 7) === 'twitter' ? 'name' : 'property';
type = property.substring(0, 7) === 'twitter' ? 'name' : 'property';
head.push('<meta ' + type + '="' + property + '" content="' + content + '" />');
}
});
@ -157,6 +158,12 @@ ghost_head = function (options) {
head.push('<meta name="generator" content="Ghost ' + trimmedVersion + '" />');
head.push('<link rel="alternate" type="application/rss+xml" title="' +
title + '" href="' + config.urlFor('rss', null, true) + '" />');
}).then(function () {
return api.settings.read({key: 'ghost_head'});
}).then(function (response) {
head.push(response.settings[0].value);
return head;
}).then(function (head) {
return filters.doFilter('ghost_head', head);
}).then(function (head) {
var headString = _.reduce(head, function (memo, item) { return memo + '\n ' + item; }, '');

View file

@ -1,48 +1,103 @@
/*globals describe, before, afterEach, it*/
/*globals describe, before, beforeEach, afterEach, it*/
/*jshint expr:true*/
var should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
rewire = require('rewire'),
hbs = require('express-hbs'),
utils = require('./utils'),
// Stuff we are testing
handlebars = hbs.handlebars,
helpers = rewire('../../../server/helpers');
helpers = rewire('../../../server/helpers'),
api = require('../../../server/api');
describe('{{ghost_foot}} helper', function () {
var sandbox;
before(function () {
utils.loadHelpers();
});
afterEach(function () {
sandbox.restore();
utils.restoreConfig();
helpers.__set__('utils.isProduction', false);
});
it('has loaded ghost_foot helper', function () {
should.exist(handlebars.helpers.ghost_foot);
describe('without Code Injection', function () {
beforeEach(function () {
sandbox = sinon.sandbox.create();
sandbox.stub(api.settings, 'read', function () {
return Promise.resolve({
settings: [{value: ''}]
});
});
});
it('has loaded ghost_foot helper', function () {
should.exist(handlebars.helpers.ghost_foot);
});
it('outputs correct jquery for development mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.js\?v=abc"><\/script>/);
done();
}).catch(done);
});
it('outputs correct jquery for production mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
helpers.__set__('utils.isProduction', true);
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.min.js\?v=abc"><\/script>/);
done();
}).catch(done);
});
});
it('outputs correct jquery for development mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
describe('with Code Injection', function () {
beforeEach(function () {
sandbox = sinon.sandbox.create();
sandbox.stub(api.settings, 'read', function () {
return Promise.resolve({
settings: [{value: '<script></script>'}]
});
});
});
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.js\?v=abc"><\/script>/);
afterEach(function () {
sandbox.restore();
});
done();
}).catch(done);
});
it('outputs correct jquery for development mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
it('outputs correct jquery for production mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
helpers.__set__('utils.isProduction', true);
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.js\?v=abc"><\/script> <script><\/script>/);
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.min.js\?v=abc"><\/script>/);
done();
}).catch(done);
});
done();
}).catch(done);
it('outputs correct jquery for production mode', function (done) {
utils.overrideConfig({assetHash: 'abc'});
helpers.__set__('utils.isProduction', true);
helpers.ghost_foot.call().then(function (rendered) {
should.exist(rendered);
rendered.string.should.match(/<script src=".*\/public\/jquery.min.js\?v=abc"><\/script> <script><\/script>/);
done();
}).catch(done);
});
});
});

View file

@ -1,15 +1,20 @@
/*globals describe, before, after, it*/
/*globals describe, before, after, afterEach, beforeEach, it*/
/*jshint expr:true*/
var should = require('should'),
sinon = require('sinon'),
Promise = require('bluebird'),
hbs = require('express-hbs'),
utils = require('./utils'),
moment = require('moment'),
// Stuff we are testing
handlebars = hbs.handlebars,
helpers = require('../../../server/helpers');
helpers = require('../../../server/helpers'),
api = require('../../../server/api');
describe('{{ghost_head}} helper', function () {
var sandbox;
before(function () {
utils.loadHelpers();
utils.overrideConfig({
@ -24,306 +29,356 @@ describe('{{ghost_head}} helper', function () {
utils.restoreConfig();
});
it('has loaded ghost_head helper', function () {
should.exist(handlebars.helpers.ghost_head);
});
it('returns meta tag string', function (done) {
helpers.ghost_head.call({version: '0.3.0', post: false}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns meta tag string even if version is invalid', function (done) {
helpers.ghost_head.call({version: '0.9'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.9" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data on post page with author image and post cover image', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: '/content/images/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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
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 \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="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n "keywords": "tag1, tag2, tag3",\n' +
' "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data if metaTitle and metaDescription have double quotes', function (done) {
var post = {
meta_description: 'blog "test" description',
title: 'title',
meta_title: 'Welcome to Ghost "test"',
image: '/content/images/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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
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 \n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost &quot;test&quot;" />\n' +
' <meta property="og:description" content="blog &quot;test&quot; description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost &quot;test&quot;" />\n' +
' <meta name="twitter:description" content="blog &quot;test&quot; description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost &quot;test&quot;",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n "keywords": "tag1, tag2, tag3",\n' +
' "description": "blog &quot;test&quot; description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data without tags if there are no tags', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: '/content/images/test-image.png',
published_at: moment('2008-05-31T19:18:15').toISOString(),
updated_at: moment('2014-10-06T15:23:54').toISOString(),
tags: [],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
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 \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="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n' +
' "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data on post page with null author image and post cover image', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: null,
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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: null,
website: 'http://authorwebsite.com'
}
};
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 \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="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "keywords": "tag1, tag2, tag3",\n "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('does not return structured data if useStructuredData is set to false in config file', function (done) {
utils.overrideConfig({
privacy: {
useStructuredData: false
}
describe('without Code Injection', function () {
beforeEach(function () {
sandbox = sinon.sandbox.create();
sandbox.stub(api.settings, 'read', function () {
return Promise.resolve({
settings: [
{value: ''}
]
});
});
});
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: 'content/images/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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: 'content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
afterEach(function () {
sandbox.restore();
});
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 name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
it('has loaded ghost_head helper', function () {
should.exist(handlebars.helpers.ghost_head);
});
done();
}).catch(done);
});
it('returns meta tag string', function (done) {
helpers.ghost_head.call({version: '0.3.0', post: false}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
it('returns canonical URL', function (done) {
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/about/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
done();
}).catch(done);
});
it('returns meta tag string even if version is invalid', function (done) {
helpers.ghost_head.call({version: '0.9'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.9" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
it('returns next & prev URL correctly for middle page', function (done) {
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('<link rel="canonical" href="http://testurl.com/page/3/" />\n' +
' <link rel="prev" href="http://testurl.com/page/2/" />\n' +
' <link rel="next" href="http://testurl.com/page/4/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
done();
}).catch(done);
});
it('returns next & prev URL correctly for second page', function (done) {
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('<link rel="canonical" href="http://testurl.com/page/2/" />\n' +
' <link rel="prev" href="http://testurl.com/" />\n' +
' <link rel="next" href="http://testurl.com/page/3/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data on post page with author image and post cover image', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: '/content/images/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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
describe('with /blog subdirectory', function () {
before(function () {
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 \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="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n "keywords": "tag1, tag2, tag3",\n' +
' "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data if metaTitle and metaDescription have double quotes', function (done) {
var post = {
meta_description: 'blog "test" description',
title: 'title',
meta_title: 'Welcome to Ghost "test"',
image: '/content/images/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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
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 \n' +
' <meta property="og:site_name" content="Ghost" />\n' +
' <meta property="og:type" content="article" />\n' +
' <meta property="og:title" content="Welcome to Ghost &quot;test&quot;" />\n' +
' <meta property="og:description" content="blog &quot;test&quot; description..." />\n' +
' <meta property="og:url" content="http://testurl.com/post/" />\n' +
' <meta property="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n' +
' <meta property="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost &quot;test&quot;" />\n' +
' <meta name="twitter:description" content="blog &quot;test&quot; description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost &quot;test&quot;",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n "keywords": "tag1, tag2, tag3",\n' +
' "description": "blog &quot;test&quot; description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data without tags if there are no tags', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: '/content/images/test-image.png',
published_at: moment('2008-05-31T19:18:15').toISOString(),
updated_at: moment('2014-10-06T15:23:54').toISOString(),
tags: [],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: '/content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
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 \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="og:image" content="http://testurl.com/content/images/test-image.png" />\n' +
' <meta property="article:published_time" content="' + post.published_at + '" />\n' +
' <meta property="article:modified_time" content="' + post.updated_at + '" />\n \n' +
' <meta name="twitter:card" content="summary_large_image" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n' +
' <meta name="twitter:image:src" content="http://testurl.com/content/images/test-image.png" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' \"image\": \"http://testurl.com/content/images/test-author-image.png\",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "image": "http://testurl.com/content/images/test-image.png",\n' +
' "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns structured data on post page with null author image and post cover image', function (done) {
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: null,
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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: null,
website: 'http://authorwebsite.com'
}
};
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 \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="article:tag" content="tag1" />\n' +
' <meta property="article:tag" content="tag2" />\n' +
' <meta property="article:tag" content="tag3" />\n \n' +
' <meta name="twitter:card" content="summary" />\n' +
' <meta name="twitter:title" content="Welcome to Ghost" />\n' +
' <meta name="twitter:description" content="blog description..." />\n' +
' <meta name="twitter:url" content="http://testurl.com/post/" />\n \n' +
' <script type=\"application/ld+json\">\n{\n' +
' "@context": "http://schema.org",\n "@type": "Article",\n "publisher": "Ghost",\n' +
' "author": {\n "@type": "Person",\n "name": "Author name",\n ' +
' "url": "http://testurl.com/author/Author",\n "sameAs": "http://authorwebsite.com"\n ' +
'},\n "headline": "Welcome to Ghost",\n "url": "http://testurl.com/post/",\n' +
' "datePublished": "' + post.published_at + '",\n "dateModified": "' + post.updated_at + '",\n' +
' "keywords": "tag1, tag2, tag3",\n "description": "blog description..."\n}\n </script>\n\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('does not return structured data if useStructuredData is set to false in config file', function (done) {
utils.overrideConfig({
url: 'http://testurl.com/blog/',
privacy: {
useStructuredData: false
}
});
var post = {
meta_description: 'blog description',
title: 'Welcome to Ghost',
image: 'content/images/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'}],
author: {
name: 'Author name',
url: 'http//:testauthorurl.com',
slug: 'Author',
image: 'content/images/test-author-image.png',
website: 'http://authorwebsite.com'
}
};
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 name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns canonical URL', function (done) {
helpers.ghost_head.call({version: '0.3.0', relativeUrl: '/about/'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/about/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns next & prev URL correctly for middle page', function (done) {
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('<link rel="canonical" href="http://testurl.com/page/3/" />\n' +
' <link rel="prev" href="http://testurl.com/page/2/" />\n' +
' <link rel="next" href="http://testurl.com/page/4/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
it('returns next & prev URL correctly for second page', function (done) {
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('<link rel="canonical" href="http://testurl.com/page/2/" />\n' +
' <link rel="prev" href="http://testurl.com/" />\n' +
' <link rel="next" href="http://testurl.com/page/3/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />');
done();
}).catch(done);
});
describe('with /blog subdirectory', function () {
before(function () {
utils.overrideConfig({
url: 'http://testurl.com/blog/',
theme: {
title: 'Ghost'
}
});
});
after(function () {
utils.restoreConfig();
});
it('returns correct rss url with subdirectory', function (done) {
helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/blog/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" ' +
'href="http://testurl.com/blog/rss/" />');
done();
}).catch(done);
});
});
});
describe('with Code Injection', function () {
before(function () {
sandbox = sinon.sandbox.create();
sandbox.stub(api.settings, 'read', function () {
return Promise.resolve({
settings: [{value: '<style>body {background: red;}</style>'}]
});
});
utils.overrideConfig({
url: 'http://testurl.com/',
theme: {
title: 'Ghost'
}
@ -331,16 +386,17 @@ describe('{{ghost_head}} helper', function () {
});
after(function () {
sandbox.restore();
utils.restoreConfig();
});
it('returns correct rss url with subdirectory', function (done) {
helpers.ghost_head.call({version: '0.3.0'}).then(function (rendered) {
it('returns meta tag plus injected code', function (done) {
helpers.ghost_head.call({version: '0.3.0', post: false}).then(function (rendered) {
should.exist(rendered);
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/blog/" />\n' +
rendered.string.should.equal('<link rel="canonical" href="http://testurl.com/" />\n' +
' <meta name="generator" content="Ghost 0.3" />\n' +
' <link rel="alternate" type="application/rss+xml" title="Ghost" ' +
'href="http://testurl.com/blog/rss/" />');
' <link rel="alternate" type="application/rss+xml" title="Ghost" href="http://testurl.com/rss/" />\n' +
' <style>body {background: red;}</style>');
done();
}).catch(done);