Code Injection - adds perms, shortcuts, icon, flag

refs #1993

- adds ctrl/cmd+s for save
- adds config flag
- adds icon on settings page, puts items in the right order
- sorts out permissions for all settings pages with consistent configuration
This commit is contained in:
Hannah Wolfe 2014-11-28 11:09:45 +00:00
parent 60effc1b51
commit 904918d5cc
8 changed files with 487 additions and 335 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

@ -2,9 +2,15 @@ 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;
var SettingsCodeInjectionRoute = AuthenticatedRoute.extend(styleBody, loadingIndicator, CurrentUserSettings, {
shortcuts[ctrlOrCmd + '+s'] = {action: 'save'};
SettingsCodeInjectionRoute = AuthenticatedRoute.extend(styleBody, loadingIndicator, CurrentUserSettings, ShortcutsRoute, {
classNames: ['settings-view-code'],
beforeModel: function () {
@ -17,6 +23,14 @@ var SettingsCodeInjectionRoute = AuthenticatedRoute.extend(styleBody, loadingInd
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');
}
}
});

View File

@ -6,26 +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}}
{{#unless session.user.isEditor}}
{{gh-activating-list-item route="settings.code-injection" title="Code Injection" classNames="settings-nav-code"}}
{{/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

@ -23,7 +23,7 @@
<div class="form-group">
<label for="blog-header">Blog Footer</label>
<p>Code here will be injected to the \{{ghost_foot}} helper at the top of your page</p>
<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>

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

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