Refactor URL builder

refs #1833

- Refactor url generation to use a base urlJoin method
- urlJoin handles slash de-duping and subdir de-duping
- fixes sitemap xml option
This commit is contained in:
Hannah Wolfe 2015-04-10 16:59:38 +01:00
parent f233d97ce3
commit 2700bfa4cc
4 changed files with 308 additions and 145 deletions

View File

@ -28,6 +28,7 @@ function ConfigManager(config) {
this._config = {}; this._config = {};
// Allow other modules to be externally accessible. // Allow other modules to be externally accessible.
this.urlJoin = configUrl.urlJoin;
this.urlFor = configUrl.urlFor; this.urlFor = configUrl.urlFor;
this.urlPathForPost = configUrl.urlPathForPost; this.urlPathForPost = configUrl.urlPathForPost;

View File

@ -15,6 +15,47 @@ function setConfig(config) {
ghostConfig = config; ghostConfig = config;
} }
function getBaseUrl(secure) {
return (secure && ghostConfig.urlSSL) ? ghostConfig.urlSSL : ghostConfig.url;
}
function urlJoin() {
var args = Array.prototype.slice.call(arguments),
prefixDoubleSlash = false,
subdir = ghostConfig.paths.subdir.replace(/\//g, ''),
subdirRegex,
url;
// Remove empty item at the beginning
if (args[0] === '') {
args.shift();
}
// Handle schemeless protocols
if (args[0].indexOf('//') === 0) {
prefixDoubleSlash = true;
}
// join the elements using a slash
url = args.join('/');
// Fix multiple slashes
url = url.replace(/(^|[^:])\/\/+/g, '$1/');
// Put the double slash back at the beginning if this was a schemeless protocol
if (prefixDoubleSlash) {
url = url.replace(/^\//, '//');
}
// Deduplicate subdirectory
if (subdir) {
subdirRegex = new RegExp(subdir + '\/' + subdir);
url = url.replace(subdirRegex, subdir);
}
return url;
}
// ## createUrl // ## createUrl
// Simple url creation from a given path // Simple url creation from a given path
// Ensures that our urls contain the subdirectory if there is one // Ensures that our urls contain the subdirectory if there is one
@ -32,25 +73,16 @@ function setConfig(config) {
function createUrl(urlPath, absolute, secure) { function createUrl(urlPath, absolute, secure) {
urlPath = urlPath || '/'; urlPath = urlPath || '/';
absolute = absolute || false; absolute = absolute || false;
var output = '', baseUrl; var base;
// create base of url, always ends without a slash // create base of url, always ends without a slash
if (absolute) { if (absolute) {
baseUrl = (secure && ghostConfig.urlSSL) ? ghostConfig.urlSSL : ghostConfig.url; base = getBaseUrl(secure);
output += baseUrl.replace(/\/$/, '');
} else { } else {
output += ghostConfig.paths.subdir; base = ghostConfig.paths.subdir;
} }
// Remove double subdirectory return urlJoin(base, urlPath);
if (urlPath.indexOf(ghostConfig.paths.subdir) === 0) {
urlPath = urlPath.replace(ghostConfig.paths.subdir, '');
}
// append the path, always starts and ends with a slash
output += urlPath;
return output;
} }
// ## urlPathForPost // ## urlPathForPost
@ -114,7 +146,8 @@ function urlFor(context, data, absolute) {
knownPaths = { knownPaths = {
home: '/', home: '/',
rss: '/rss/', rss: '/rss/',
api: '/ghost/api/v0.1' api: '/ghost/api/v0.1',
sitemap_xsl: '/sitemap.xsl'
}; };
// Make data properly optional // Make data properly optional
@ -134,10 +167,10 @@ function urlFor(context, data, absolute) {
urlPath = data.post.url; urlPath = data.post.url;
secure = data.secure; secure = data.secure;
} else if (context === 'tag' && data.tag) { } else if (context === 'tag' && data.tag) {
urlPath = '/' + ghostConfig.routeKeywords.tag + '/' + data.tag.slug + '/'; urlPath = urlJoin('/', ghostConfig.routeKeywords.tag, data.tag.slug, '/');
secure = data.tag.secure; secure = data.tag.secure;
} else if (context === 'author' && data.author) { } else if (context === 'author' && data.author) {
urlPath = '/' + ghostConfig.routeKeywords.author + '/' + data.author.slug + '/'; urlPath = urlJoin('/', ghostConfig.routeKeywords.author, data.author.slug, '/');
secure = data.author.secure; secure = data.author.secure;
} else if (context === 'image' && data.image) { } else if (context === 'image' && data.image) {
urlPath = data.image; urlPath = data.image;
@ -148,18 +181,14 @@ function urlFor(context, data, absolute) {
if (absolute) { if (absolute) {
// Remove the sub-directory from the URL because ghostConfig will add it back. // Remove the sub-directory from the URL because ghostConfig will add it back.
urlPath = urlPath.replace(new RegExp('^' + ghostConfig.paths.subdir), ''); urlPath = urlPath.replace(new RegExp('^' + ghostConfig.paths.subdir), '');
baseUrl = (secure && ghostConfig.urlSSL) ? ghostConfig.urlSSL : ghostConfig.url; baseUrl = getBaseUrl(secure).replace(/\/$/, '');
baseUrl = baseUrl.replace(/\/$/, '');
urlPath = baseUrl + urlPath; urlPath = baseUrl + urlPath;
} }
return urlPath; return urlPath;
} else if (context === 'sitemap-xsl') {
absolute = true;
urlPath = '/sitemap.xsl';
} else if (context === 'nav' && data.nav) { } else if (context === 'nav' && data.nav) {
urlPath = data.nav.url; urlPath = data.nav.url;
baseUrl = (secure && ghostConfig.urlSSL) ? ghostConfig.urlSSL : ghostConfig.url; baseUrl = getBaseUrl(secure);
hostname = baseUrl.split('//')[1] + ghostConfig.paths.subdir; hostname = baseUrl.split('//')[1] + ghostConfig.paths.subdir;
if (urlPath.indexOf(hostname) > -1 && urlPath.indexOf('.' + hostname) === -1) { if (urlPath.indexOf(hostname) > -1 && urlPath.indexOf('.' + hostname) === -1) {
// make link relative to account for possible // make link relative to account for possible
@ -187,5 +216,6 @@ function urlFor(context, data, absolute) {
} }
module.exports.setConfig = setConfig; module.exports.setConfig = setConfig;
module.exports.urlJoin = urlJoin;
module.exports.urlFor = urlFor; module.exports.urlFor = urlFor;
module.exports.urlPathForPost = urlPathForPost; module.exports.urlPathForPost = urlPathForPost;

View File

@ -3,10 +3,10 @@ var config = require('../../../config'),
utils = { utils = {
getDeclarations: function () { getDeclarations: function () {
var baseUrl = config.urlFor('sitemap-xsl'); var baseUrl = config.urlFor('sitemap_xsl', true);
baseUrl = baseUrl.replace(/^(http:|https:)/, ''); baseUrl = baseUrl.replace(/^(http:|https:)/, '');
return '<?xml version="1.0" encoding="UTF-8"?>' + return '<?xml version="1.0" encoding="UTF-8"?>' +
'<?xml-stylesheet type="text/xsl" href="' + baseUrl + 'sitemap.xsl"?>'; '<?xml-stylesheet type="text/xsl" href="' + baseUrl + '"?>';
} }
}; };

View File

@ -176,137 +176,269 @@ describe('Config', function () {
}); });
}); });
describe('urlFor', function () { describe('Url', function () {
before(function () { describe('urlJoin', function () {
resetConfig(); before(function () {
resetConfig();
});
afterEach(function () {
resetConfig();
});
it('should deduplicate slashes', function () {
config.set({url: 'http://my-ghost-blog.com/'});
config.urlJoin('/', '/my/', '/blog/').should.equal('/my/blog/');
config.urlJoin('/', '//my/', '/blog/').should.equal('/my/blog/');
config.urlJoin('/', '/', '/').should.equal('/');
});
it('should not deduplicate slashes in protocol', function () {
config.set({url: 'http://my-ghost-blog.com/'});
config.urlJoin('http://myurl.com', '/rss').should.equal('http://myurl.com/rss');
config.urlJoin('https://myurl.com/', '/rss').should.equal('https://myurl.com/rss');
});
it('should permit schemeless protocol', function () {
config.set({url: 'http://my-ghost-blog.com/'});
config.urlJoin('/', '/').should.equal('/');
config.urlJoin('//myurl.com', '/rss').should.equal('//myurl.com/rss');
config.urlJoin('//myurl.com/', '/rss').should.equal('//myurl.com/rss');
config.urlJoin('//myurl.com//', 'rss').should.equal('//myurl.com/rss');
config.urlJoin('', '//myurl.com', 'rss').should.equal('//myurl.com/rss');
});
it('should deduplicate subdir', function () {
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlJoin('blog', 'blog/about').should.equal('blog/about');
config.urlJoin('blog/', 'blog/about').should.equal('blog/about');
});
}); });
afterEach(function () { describe('urlFor', function () {
resetConfig(); before(function () {
resetConfig();
});
afterEach(function () {
resetConfig();
});
it('should return the home url with no options', function () {
config.urlFor().should.equal('/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor().should.equal('/blog/');
config.set({url: 'http://my-ghost-blog.com/blog/'});
config.urlFor().should.equal('/blog/');
});
it('should return home url when asked for', function () {
var testContext = 'home';
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
config.set({url: 'http://my-ghost-blog.com/'});
config.urlFor(testContext).should.equal('/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
config.set({url: 'http://my-ghost-blog.com/blog/'});
config.urlFor(testContext).should.equal('/blog/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
});
it('should return rss url when asked for', function () {
var testContext = 'rss';
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/rss/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/rss/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/rss/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/rss/');
});
it('should return url for a random path when asked for', function () {
var testContext = {relativeUrl: '/about/'};
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/about/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
});
it('should deduplicate subdirectories in paths', function () {
var testContext = {relativeUrl: '/blog/about/'};
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/blog/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
config.set({url: 'http://my-ghost-blog.com/blog/'});
config.urlFor(testContext).should.equal('/blog/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
});
it('should return url for a post from post object', function () {
var testContext = 'post',
testData = {post: testUtils.DataGenerator.Content.posts[2]};
// url is now provided on the postmodel, permalinkSetting tests are in the model_post_spec.js test
testData.post.url = '/short-and-sweet/';
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext, testData).should.equal('/short-and-sweet/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/short-and-sweet/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext, testData).should.equal('/blog/short-and-sweet/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/short-and-sweet/');
});
it('should return url for a tag when asked for', function () {
var testContext = 'tag',
testData = {tag: testUtils.DataGenerator.Content.tags[0]};
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext, testData).should.equal('/tag/kitchen-sink/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/tag/kitchen-sink/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext, testData).should.equal('/blog/tag/kitchen-sink/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/tag/kitchen-sink/');
});
it('should return url for an author when asked for', function () {
var testContext = 'author',
testData = {author: testUtils.DataGenerator.Content.users[0]};
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext, testData).should.equal('/author/joe-bloggs/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/author/joe-bloggs/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext, testData).should.equal('/blog/author/joe-bloggs/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/author/joe-bloggs/');
});
it('should return url for an image when asked for', function () {
var testContext = 'image',
testData;
config.set({url: 'http://my-ghost-blog.com'});
testData = {image: '/content/images/my-image.jpg'};
config.urlFor(testContext, testData).should.equal('/content/images/my-image.jpg');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/content/images/my-image.jpg');
testData = {image: 'http://placekitten.com/500/200'};
config.urlFor(testContext, testData).should.equal('http://placekitten.com/500/200');
config.urlFor(testContext, testData, true).should.equal('http://placekitten.com/500/200');
testData = {image: '/blog/content/images/my-image2.jpg'};
config.urlFor(testContext, testData).should.equal('/blog/content/images/my-image2.jpg');
// We don't make image urls absolute if they don't look like images relative to the image path
config.urlFor(testContext, testData, true).should.equal('/blog/content/images/my-image2.jpg');
config.set({url: 'http://my-ghost-blog.com/blog/'});
testData = {image: '/content/images/my-image3.jpg'};
config.urlFor(testContext, testData).should.equal('/content/images/my-image3.jpg');
// We don't make image urls absolute if they don't look like images relative to the image path
config.urlFor(testContext, testData, true).should.equal('/content/images/my-image3.jpg');
testData = {image: '/blog/content/images/my-image4.jpg'};
config.urlFor(testContext, testData).should.equal('/blog/content/images/my-image4.jpg');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/content/images/my-image4.jpg');
});
it('should return a url for a nav item when asked for it', function () {
var testContext = 'nav',
testData;
config.set({url: 'http://my-ghost-blog.com', urlSSL: 'https://my-ghost-blog.com'});
testData = {nav: {url: 'http://my-ghost-blog.com/short-and-sweet/'}};
config.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/short-and-sweet/');
testData = {nav: {url: 'http://my-ghost-blog.com/short-and-sweet/'}, secure: true};
config.urlFor(testContext, testData).should.equal('https://my-ghost-blog.com/short-and-sweet/');
testData = {nav: {url: 'http://sub.my-ghost-blog.com/'}};
config.urlFor(testContext, testData).should.equal('http://sub.my-ghost-blog.com/');
config.set({url: 'http://my-ghost-blog.com/blog'});
testData = {nav: {url: 'http://my-ghost-blog.com/blog/short-and-sweet/'}};
config.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/blog/short-and-sweet/');
});
it('should return other known paths when requested', function () {
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor('sitemap_xsl').should.equal('/sitemap.xsl');
config.urlFor('sitemap_xsl', true).should.equal('http://my-ghost-blog.com/sitemap.xsl');
config.urlFor('api').should.equal('/ghost/api/v0.1');
config.urlFor('api', true).should.equal('http://my-ghost-blog.com/ghost/api/v0.1');
});
}); });
it('should return the home url with no options', function () { describe('urlPathForPost', function () {
config.urlFor().should.equal('/'); it('should output correct url for post', function () {
config.set({url: 'http://my-ghost-blog.com/blog'}); var permalinkSetting = '/:slug/',
config.urlFor().should.equal('/blog/');
});
it('should return home url when asked for', function () {
var testContext = 'home';
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/');
});
it('should return rss url when asked for', function () {
var testContext = 'rss';
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/rss/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/rss/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/rss/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/rss/');
});
it('should return url for a random path when asked for', function () {
var testContext = {relativeUrl: '/about/'};
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext).should.equal('/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/about/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext).should.equal('/blog/about/');
config.urlFor(testContext, true).should.equal('http://my-ghost-blog.com/blog/about/');
});
it('should return url for a post from post object', function () {
var testContext = 'post',
testData = {post: testUtils.DataGenerator.Content.posts[2]};
// url is now provided on the postmodel, permalinkSetting tests are in the model_post_spec.js test
testData.post.url = '/short-and-sweet/';
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext, testData).should.equal('/short-and-sweet/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/short-and-sweet/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext, testData).should.equal('/blog/short-and-sweet/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/short-and-sweet/');
});
it('should return url for a tag when asked for', function () {
var testContext = 'tag',
testData = {tag: testUtils.DataGenerator.Content.tags[0]};
config.set({url: 'http://my-ghost-blog.com'});
config.urlFor(testContext, testData).should.equal('/tag/kitchen-sink/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/tag/kitchen-sink/');
config.set({url: 'http://my-ghost-blog.com/blog'});
config.urlFor(testContext, testData).should.equal('/blog/tag/kitchen-sink/');
config.urlFor(testContext, testData, true).should.equal('http://my-ghost-blog.com/blog/tag/kitchen-sink/');
});
it('should return a url for a nav item when asked for it', function () {
var testContext = 'nav',
testData;
config.set({url: 'http://my-ghost-blog.com', urlSSL: 'https://my-ghost-blog.com'});
testData = {nav: {url: 'http://my-ghost-blog.com/short-and-sweet/'}};
config.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/short-and-sweet/');
testData = {nav: {url: 'http://my-ghost-blog.com/short-and-sweet/'}, secure: true};
config.urlFor(testContext, testData).should.equal('https://my-ghost-blog.com/short-and-sweet/');
testData = {nav: {url: 'http://sub.my-ghost-blog.com/'}};
config.urlFor(testContext, testData).should.equal('http://sub.my-ghost-blog.com/');
config.set({url: 'http://my-ghost-blog.com/blog'});
testData = {nav: {url: 'http://my-ghost-blog.com/blog/short-and-sweet/'}};
config.urlFor(testContext, testData).should.equal('http://my-ghost-blog.com/blog/short-and-sweet/');
});
});
describe('urlPathForPost', function () {
it('should output correct url for post', function () {
var permalinkSetting = '/:slug/',
/*jshint unused:false*/ /*jshint unused:false*/
testData = testUtils.DataGenerator.Content.posts[2], testData = testUtils.DataGenerator.Content.posts[2],
postLink = '/short-and-sweet/'; postLink = '/short-and-sweet/';
// next test // next test
config.urlPathForPost(testData, permalinkSetting).should.equal(postLink); config.urlPathForPost(testData, permalinkSetting).should.equal(postLink);
}); });
it('should output correct url for post with date permalink', function () { it('should output correct url for post with date permalink', function () {
var permalinkSetting = '/:year/:month/:day/:slug/', var permalinkSetting = '/:year/:month/:day/:slug/',
/*jshint unused:false*/ /*jshint unused:false*/
testData = testUtils.DataGenerator.Content.posts[2], testData = testUtils.DataGenerator.Content.posts[2],
today = testData.published_at, today = testData.published_at,
dd = ('0' + today.getDate()).slice(-2), dd = ('0' + today.getDate()).slice(-2),
mm = ('0' + (today.getMonth() + 1)).slice(-2), mm = ('0' + (today.getMonth() + 1)).slice(-2),
yyyy = today.getFullYear(), yyyy = today.getFullYear(),
postLink = '/' + yyyy + '/' + mm + '/' + dd + '/short-and-sweet/'; postLink = '/' + yyyy + '/' + mm + '/' + dd + '/short-and-sweet/';
// next test // next test
config.urlPathForPost(testData, permalinkSetting).should.equal(postLink); config.urlPathForPost(testData, permalinkSetting).should.equal(postLink);
}); });
it('should output correct url for page with date permalink', function () { it('should output correct url for page with date permalink', function () {
var permalinkSetting = '/:year/:month/:day/:slug/', var permalinkSetting = '/:year/:month/:day/:slug/',
/*jshint unused:false*/ /*jshint unused:false*/
testData = testUtils.DataGenerator.Content.posts[5], testData = testUtils.DataGenerator.Content.posts[5],
postLink = '/static-page-test/'; postLink = '/static-page-test/';
// next test // next test
config.urlPathForPost(testData, permalinkSetting).should.equal(postLink); config.urlPathForPost(testData, permalinkSetting).should.equal(postLink);
});
it('should output correct url for post with complex permalink', function () {
var permalinkSetting = '/:year/:id/:author/',
/*jshint unused:false*/
testData = _.extend(
{}, testUtils.DataGenerator.Content.posts[2], {id: 3}, {author: {slug: 'joe-bloggs'}}
),
today = testData.published_at,
yyyy = today.getFullYear(),
postLink = '/' + yyyy + '/3/joe-bloggs/';
// next test
config.urlPathForPost(testData, permalinkSetting).should.equal(postLink);
});
}); });
}); });