diff --git a/core/server/config/index.js b/core/server/config/index.js
index f208bdcaa2..8d28900895 100644
--- a/core/server/config/index.js
+++ b/core/server/config/index.js
@@ -33,6 +33,7 @@ function ConfigManager(config) {
this.urlFor = configUrl.urlFor;
this.urlPathForPost = configUrl.urlPathForPost;
this.apiUrl = configUrl.apiUrl;
+ this.getBaseUrl = configUrl.getBaseUrl;
// If we're given an initial config object then we can set it.
if (config && _.isObject(config)) {
@@ -214,7 +215,10 @@ ConfigManager.prototype.set = function (config) {
// Used by generateSlug to generate slugs for posts, tags, users, ..
// reserved slugs are reserved but can be extended/removed by apps
// protected slugs cannot be changed or removed
- reserved: ['admin', 'app', 'apps', 'archive', 'archives', 'categories', 'category', 'dashboard', 'feed', 'ghost-admin', 'login', 'logout', 'page', 'pages', 'post', 'posts', 'public', 'register', 'setup', 'signin', 'signout', 'signup', 'user', 'users', 'wp-admin', 'wp-login'],
+ reserved: ['admin', 'app', 'apps', 'archive', 'archives', 'categories',
+ 'category', 'dashboard', 'feed', 'ghost-admin', 'login', 'logout',
+ 'page', 'pages', 'post', 'posts', 'public', 'register', 'setup',
+ 'signin', 'signout', 'signup', 'user', 'users', 'wp-admin', 'wp-login'],
protected: ['ghost', 'rss']
},
uploads: {
diff --git a/core/server/config/url.js b/core/server/config/url.js
index c44fc982ca..3f413083ce 100644
--- a/core/server/config/url.js
+++ b/core/server/config/url.js
@@ -18,7 +18,15 @@ function setConfig(config) {
}
function getBaseUrl(secure) {
- return (secure && ghostConfig.urlSSL) ? ghostConfig.urlSSL : ghostConfig.url;
+ if (secure && ghostConfig.urlSSL) {
+ return ghostConfig.urlSSL;
+ } else {
+ if (secure) {
+ return ghostConfig.url.replace('http://', 'https://');
+ } else {
+ return ghostConfig.url;
+ }
+ }
}
function urlJoin() {
@@ -190,6 +198,7 @@ function urlFor(context, data, absolute) {
return urlPath;
} else if (context === 'nav' && data.nav) {
urlPath = data.nav.url;
+ secure = data.nav.secure || secure;
baseUrl = getBaseUrl(secure);
hostname = baseUrl.split('//')[1] + ghostConfig.paths.subdir;
if (urlPath.indexOf(hostname) > -1
@@ -242,3 +251,4 @@ module.exports.urlJoin = urlJoin;
module.exports.urlFor = urlFor;
module.exports.urlPathForPost = urlPathForPost;
module.exports.apiUrl = apiUrl;
+module.exports.getBaseUrl = getBaseUrl;
diff --git a/core/server/helpers/ghost_head.js b/core/server/helpers/ghost_head.js
index 2aaf0282b4..f5e76bbcfe 100644
--- a/core/server/helpers/ghost_head.js
+++ b/core/server/helpers/ghost_head.js
@@ -109,6 +109,7 @@ function addContextMetaData(context, data, metaData) {
function initMetaData(context, data, results) {
var metaData = {
url: results.url,
+ canonicalUrl: results.canonicalUrl,
metaDescription: results.meta_description || null,
metaTitle: results.meta_title,
coverImage: results.image,
@@ -148,7 +149,7 @@ function getStructuredData(metaData) {
'og:type': metaData.ogType,
'og:title': metaData.metaTitle,
'og:description': metaData.metaDescription,
- 'og:url': metaData.url,
+ 'og:url': metaData.canonicalUrl,
'og:image': metaData.coverImage,
'article:published_time': metaData.publishedDate,
'article:modified_time': metaData.modifiedDate,
@@ -156,7 +157,7 @@ function getStructuredData(metaData) {
'twitter:card': metaData.card,
'twitter:title': metaData.metaTitle,
'twitter:description': metaData.metaDescription,
- 'twitter:url': metaData.url,
+ 'twitter:url': metaData.canonicalUrl,
'twitter:image:src': metaData.coverImage
};
@@ -311,6 +312,8 @@ ghost_head = function (options) {
// Store Async calls in an object of named promises
props.url = urlHelper.call(self, {hash: {absolute: true}});
+ props.canonicalUrl = config.urlJoin(config.getBaseUrl(false),
+ urlHelper.call(self, {hash: {absolute: false}}));
props.meta_description = meta_description.call(self, options);
props.meta_title = meta_title.call(self, options);
props.client = getClient();
@@ -330,7 +333,7 @@ ghost_head = function (options) {
}
// head is our main array that holds our meta data
- head.push('');
+ head.push('');
head.push('');
// Generate context driven pagination urls
@@ -359,7 +362,8 @@ ghost_head = function (options) {
head.push('');
head.push('');
+ title + '" href="' + config.urlFor('rss', {secure: self.secure},
+ true) + '" />');
}).then(function () {
return api.settings.read({key: 'ghost_head'});
}).then(function (response) {
diff --git a/core/server/helpers/navigation.js b/core/server/helpers/navigation.js
index 0834ed3e67..ff7fec2404 100644
--- a/core/server/helpers/navigation.js
+++ b/core/server/helpers/navigation.js
@@ -13,6 +13,7 @@ navigation = function (options) {
/*jshint unused:false*/
var navigationData = options.data.blog.navigation,
currentUrl = options.data.root.relativeUrl,
+ self = this,
output,
context;
@@ -49,6 +50,7 @@ navigation = function (options) {
out.label = e.label;
out.slug = _slugify(e.label);
out.url = hbs.handlebars.Utils.escapeExpression(e.url);
+ out.secure = self.secure;
return out;
});
diff --git a/core/server/helpers/url.js b/core/server/helpers/url.js
index 69c7451783..7794ad7900 100644
--- a/core/server/helpers/url.js
+++ b/core/server/helpers/url.js
@@ -12,22 +12,22 @@ url = function (options) {
var absolute = options && options.hash.absolute;
if (schema.isPost(this)) {
- return config.urlFor('post', {post: this}, absolute);
+ return config.urlFor('post', {post: this, secure: this.secure}, absolute);
}
if (schema.isTag(this)) {
- return config.urlFor('tag', {tag: this}, absolute);
+ return config.urlFor('tag', {tag: this, secure: this.secure}, absolute);
}
if (schema.isUser(this)) {
- return config.urlFor('author', {author: this}, absolute);
+ return config.urlFor('author', {author: this, secure: this.secure}, absolute);
}
if (schema.isNav(this)) {
- return config.urlFor('nav', {nav: this}, absolute);
+ return config.urlFor('nav', {nav: this, secure: this.secure}, absolute);
}
- return config.urlFor(this, absolute);
+ return config.urlFor(this, {}, absolute);
};
module.exports = url;
diff --git a/core/test/functional/routes/frontend_spec.js b/core/test/functional/routes/frontend_spec.js
index c0d9256cc3..65d68db665 100644
--- a/core/test/functional/routes/frontend_spec.js
+++ b/core/test/functional/routes/frontend_spec.js
@@ -985,11 +985,11 @@ describe('Frontend Routing', function () {
.end(doEnd(done));
});
- it('should set links to urlSSL over HTTPS', function (done) {
+ it('should set links to urlSSL over HTTPS besides canonical', function (done) {
request.get('/')
.set('X-Forwarded-Proto', 'https')
.expect(200)
- .expect(//)
+ .expect(//)
.expect(/Ghost<\/a\>/)
.end(doEnd(done));
});
diff --git a/core/test/unit/server_helpers/url_spec.js b/core/test/unit/server_helpers/url_spec.js
index f105abbaae..436e094108 100644
--- a/core/test/unit/server_helpers/url_spec.js
+++ b/core/test/unit/server_helpers/url_spec.js
@@ -64,6 +64,28 @@ describe('{{url}} helper', function () {
rendered.should.equal('http://testurl.com/slug/');
});
+ it('should output an absolute URL with https if the option is present and secure', function () {
+ rendered = helpers.url.call(
+ {html: 'content', markdown: 'ff', title: 'title', slug: 'slug',
+ url: '/slug/', created_at: new Date(0), secure: true},
+ {hash: {absolute: 'true'}}
+ );
+
+ should.exist(rendered);
+ rendered.should.equal('https://testurl.com/slug/');
+ });
+
+ it('should output an absolute URL with https if secure', function () {
+ rendered = helpers.url.call(
+ {html: 'content', markdown: 'ff', title: 'title', slug: 'slug',
+ url: '/slug/', created_at: new Date(0), secure: true},
+ {hash: {absolute: 'true'}}
+ );
+
+ should.exist(rendered);
+ rendered.should.equal('https://testurl.com/slug/');
+ });
+
it('should return the slug with a prefixed /tag/ if the context is a tag', function () {
rendered = helpers.url.call({
name: 'the tag',
@@ -109,6 +131,14 @@ describe('{{url}} helper', function () {
rendered.should.equal('http://testurl.com/bar');
});
+ it('should return an absolute url with https if context is secure', function () {
+ rendered = helpers.url.call(
+ {url: '/bar', label: 'Bar', slug: 'bar', current: true, secure: true},
+ {hash: {absolute: 'true'}});
+ should.exist(rendered);
+ rendered.should.equal('https://testurl.com/bar');
+ });
+
it('external urls should be retained in a nav context', function () {
rendered = helpers.url.call(
{url: 'http://casper.website/baz', label: 'Baz', slug: 'baz', current: true},
@@ -125,6 +155,24 @@ describe('{{url}} helper', function () {
rendered.should.equal('http://testurl.com/qux');
});
+ it('should handle hosted urls in a nav context with secure', function () {
+ rendered = helpers.url.call(
+ {url: 'http://testurl.com/qux', label: 'Qux', slug: 'qux', current: true,
+ secure: true},
+ {hash: {absolute: 'true'}});
+ should.exist(rendered);
+ rendered.should.equal('https://testurl.com/qux');
+ });
+
+ it('should handle hosted https urls in a nav context with secure', function () {
+ rendered = helpers.url.call(
+ {url: 'https://testurl.com/qux', label: 'Qux', slug: 'qux', current: true,
+ secure: true},
+ {hash: {absolute: 'true'}});
+ should.exist(rendered);
+ rendered.should.equal('https://testurl.com/qux');
+ });
+
it('should handle hosted urls with the wrong protocol in a nav context', function () {
rendered = helpers.url.call(
{url: 'https://testurl.com/quux', label: 'Quux', slug: 'quux', current: true},