diff --git a/core/client/helpers/index.js b/core/client/helpers/index.js
index 6d7b1947b2..474a1e7de0 100644
--- a/core/client/helpers/index.js
+++ b/core/client/helpers/index.js
@@ -29,8 +29,8 @@
return date;
});
- Handlebars.registerHelper('url', function () {
- return Ghost.paths.subdir;
+ Handlebars.registerHelper('adminUrl', function () {
+ return Ghost.paths.subdir + '/ghost';
});
Handlebars.registerHelper('asset', function (context, options) {
diff --git a/core/client/tpl/login.hbs b/core/client/tpl/login.hbs
index 896e1106d1..18b97e9fe5 100644
--- a/core/client/tpl/login.hbs
+++ b/core/client/tpl/login.hbs
@@ -7,6 +7,6 @@
diff --git a/core/client/tpl/preview.hbs b/core/client/tpl/preview.hbs
index 75a7c43fad..1822dbd704 100644
--- a/core/client/tpl/preview.hbs
+++ b/core/client/tpl/preview.hbs
@@ -53,7 +53,7 @@
You Haven't Written Any Posts Yet!
-
+
{{/unless}}
diff --git a/core/server/api/db.js b/core/server/api/db.js
index 290cbcc84f..b683bfc39e 100644
--- a/core/server/api/db.js
+++ b/core/server/api/db.js
@@ -6,7 +6,7 @@ var dataExport = require('../data/export'),
when = require('when'),
nodefn = require('when/node/function'),
_ = require('underscore'),
- schema = require('../data/schema'),
+ schema = require('../data/schema').tables,
configPaths = require('../config/paths'),
api = {},
diff --git a/core/server/api/index.js b/core/server/api/index.js
index 4defee7f55..3a1e669d5b 100644
--- a/core/server/api/index.js
+++ b/core/server/api/index.js
@@ -16,7 +16,7 @@ var _ = require('underscore'),
// ## Request Handlers
-function invalidateCache(req, res, result) {
+function cacheInvalidationHeader(req, result) {
var parsedUrl = req._parsedUrl.pathname.replace(/\/$/, '').split('/'),
method = req.method,
endpoint = parsedUrl[4],
@@ -30,15 +30,14 @@ function invalidateCache(req, res, result) {
} else if (endpoint === 'posts') {
cacheInvalidate = "/, /page/*, /rss/, /rss/*";
if (id && jsonResult.slug) {
- cacheInvalidate += ', /' + jsonResult.slug + '/';
+ return config.paths.urlForPost(settings, jsonResult).then(function (postUrl) {
+ return cacheInvalidate + ', ' + postUrl;
+ });
}
}
- if (cacheInvalidate) {
- res.set({
- "X-Cache-Invalidate": cacheInvalidate
- });
- }
}
+
+ return when(cacheInvalidate);
}
// ### requestHandler
@@ -52,8 +51,14 @@ requestHandler = function (apiMethod) {
};
return apiMethod.call(apiContext, options).then(function (result) {
- invalidateCache(req, res, result);
res.json(result || {});
+ return cacheInvalidationHeader(req, result).then(function (header) {
+ if (header) {
+ res.set({
+ "X-Cache-Invalidate": header
+ });
+ }
+ });
}, function (error) {
var errorCode = error.errorCode || 500,
errorMsg = {error: _.isString(error) ? error : (_.isObject(error) ? error.message : 'Unknown API Error')};
diff --git a/core/server/api/posts.js b/core/server/api/posts.js
index ec5c0fcb0e..22fd61f31b 100644
--- a/core/server/api/posts.js
+++ b/core/server/api/posts.js
@@ -106,9 +106,7 @@ posts = {
return canThis(this.user).remove.post(args.id).then(function () {
return when(posts.read({id : args.id, status: 'all'})).then(function (result) {
return dataProvider.Post.destroy(args.id).then(function () {
- var deletedObj = {};
- deletedObj.id = result.id;
- deletedObj.slug = result.slug;
+ var deletedObj = result;
return deletedObj;
});
});
diff --git a/core/server/config/paths.js b/core/server/config/paths.js
index c042c2fba6..84139b4c98 100644
--- a/core/server/config/paths.js
+++ b/core/server/config/paths.js
@@ -1,9 +1,11 @@
// Contains all path information to be used throughout
// the codebase.
-var path = require('path'),
+var moment = require('moment'),
+ path = require('path'),
when = require('when'),
url = require('url'),
+ _ = require('underscore'),
requireTree = require('../require-tree'),
appRoot = path.resolve(__dirname, '../../../'),
corePath = path.resolve(appRoot, 'core/'),
@@ -13,8 +15,9 @@ var path = require('path'),
themeDirectories = requireTree(themePath),
pluginDirectories = requireTree(pluginPath),
localPath = '',
- availableThemes,
+ configUrl = '',
+ availableThemes,
availablePlugins;
@@ -45,6 +48,7 @@ function paths() {
// TODO: remove configURL and give direct access to config object?
// TODO: not called when executing tests
function update(configURL) {
+ configUrl = configURL;
localPath = url.parse(configURL).path;
// Remove trailing slash
@@ -59,5 +63,129 @@ function update(configURL) {
});
}
+// ## createUrl
+// Simple url creation from a given path
+// Ensures that our urls contain the subdirectory if there is one
+// And are correctly formatted as either relative or absolute
+// Usage:
+// createUrl('/', true) -> http://my-ghost-blog.com/
+// E.g. /blog/ subdir
+// createUrl('/welcome-to-ghost/') -> /blog/welcome-to-ghost/
+// Parameters:
+// - urlPath - string which must start and end with a slash
+// - absolute (optional, default:false) - boolean whether or not the url should be absolute
+// Returns:
+// - a URL which always ends with a slash
+function createUrl(urlPath, absolute) {
+ urlPath = urlPath || '/';
+ absolute = absolute || false;
+
+ var output = '';
+
+ // create base of url, always ends without a slash
+ if (absolute) {
+ output += configUrl.replace(/\/$/, '');
+ } else {
+ output += paths().subdir;
+ }
+
+ // append the path, always starts and ends with a slash
+ output += urlPath;
+
+ return output;
+}
+
+// ## urlPathForPost
+// Always sync
+// Creates the url path for a post, given a post and a permalink
+// Parameters:
+// - post - a json object representing a post
+// - permalinks - a json object containing the permalinks setting
+function urlPathForPost(post, permalinks) {
+ var output = '',
+ tags = {
+ year: function () { return moment(post.published_at).format('YYYY'); },
+ month: function () { return moment(post.published_at).format('MM'); },
+ day: function () { return moment(post.published_at).format('DD'); },
+ slug: function () { return post.slug; },
+ id: function () { return post.id; }
+ };
+
+ if (post.page === 1) {
+ output += '/:slug/';
+ } else {
+ output += permalinks.value;
+ }
+
+ // replace tags like :slug or :year with actual values
+ output = output.replace(/(:[a-z]+)/g, function (match) {
+ if (_.has(tags, match.substr(1))) {
+ return tags[match.substr(1)]();
+ }
+ });
+
+ return output;
+}
+
+// ## urlFor
+// Synchronous url creation for a given context
+// Can generate a url for a named path, given path, or known object (post)
+// Determines what sort of context it has been given, and delegates to the correct generation method,
+// Finally passing to createUrl, to ensure any subdirectory is honoured, and the url is absolute if needed
+// Usage:
+// urlFor('home', true) -> http://my-ghost-blog.com/
+// E.g. /blog/ subdir
+// urlFor({relativeUrl: '/my-static-page/') -> /blog/my-static-page/
+// E.g. if post object represents welcome post, and slugs are set to standard
+// urlFor('post', {...}) -> /welcome-to-ghost/
+// E.g. if post object represents welcome post, and slugs are set to date
+// urlFor('post', {...}) -> /2014/01/01/welcome-to-ghost/
+// Parameters:
+// - context - a string, or json object describing the context for which you need a url
+// - data (optional) - a json object containing data needed to generate a url
+// - absolute (optional, default:false) - boolean whether or not the url should be absolute
+// This is probably not the right place for this, but it's the best place for now
+function urlFor(context, data, absolute) {
+ var urlPath = '/',
+ knownObjects = ['post', 'tag', 'user'],
+ knownPaths = {'home': '/', 'rss': '/rss/'}; // this will become really big
+
+ // Make data properly optional
+ if (_.isBoolean(data)) {
+ absolute = data;
+ data = null;
+ }
+
+ if (_.isObject(context) && context.relativeUrl) {
+ urlPath = context.relativeUrl;
+ } else if (_.isString(context) && _.indexOf(knownObjects, context) !== -1) {
+ // trying to create a url for an object
+ if (context === 'post' && data.post && data.permalinks) {
+ urlPath = urlPathForPost(data.post, data.permalinks);
+ }
+ // other objects are recognised but not yet supported
+ } else if (_.isString(context) && _.indexOf(_.keys(knownPaths), context) !== -1) {
+ // trying to create a url for a named path
+ urlPath = knownPaths[context] || '/';
+ }
+
+ return createUrl(urlPath, absolute);
+}
+
+// ## urlForPost
+// This method is async as we have to fetch the permalinks
+// Get the permalink setting and then get a URL for the given post
+// Parameters
+// - settings - passed reference to api.settings
+// - post - a json object representing a post
+// - absolute (optional, default:false) - boolean whether or not the url should be absolute
+function urlForPost(settings, post, absolute) {
+ return settings.read('permalinks').then(function (permalinks) {
+ return urlFor('post', {post: post, permalinks: permalinks}, absolute);
+ });
+}
+
module.exports = paths;
module.exports.update = update;
+module.exports.urlFor = urlFor;
+module.exports.urlForPost = urlForPost;
diff --git a/core/server/controllers/frontend.js b/core/server/controllers/frontend.js
index 9324119449..921fbfbfa1 100644
--- a/core/server/controllers/frontend.js
+++ b/core/server/controllers/frontend.js
@@ -25,19 +25,14 @@ frontendControllers = {
postsPerPage,
options = {};
- api.settings.read('postsPerPage').then(function (postPP) {
- postsPerPage = parseInt(postPP.value, 10);
- // No negative pages
- if (isNaN(pageParam) || pageParam < 1) {
- //redirect to 404 page?
- return res.redirect('/');
- }
- options.page = pageParam;
+ // No negative pages, or page 1
+ if (isNaN(pageParam) || pageParam < 1 || (pageParam === 1 && req.route.path === '/page/:page/')) {
+ return res.redirect(config.paths().subdir + '/');
+ }
- // Redirect '/page/1/' to '/' for all teh good SEO
- if (pageParam === 1 && req.route.path === '/page/:page/') {
- return res.redirect(config.paths().subdir + '/');
- }
+ return api.settings.read('postsPerPage').then(function (postPP) {
+ postsPerPage = parseInt(postPP.value, 10);
+ options.page = pageParam;
// No negative posts per page, must be number
if (!isNaN(postsPerPage) && postsPerPage > 0) {
@@ -138,39 +133,39 @@ frontendControllers = {
},
'rss': function (req, res, next) {
// Initialize RSS
- var siteUrl = config().url,
- pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
+ var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
feed;
+
+ // No negative pages, or page 1
+ if (isNaN(pageParam) || pageParam < 1 || (pageParam === 1 && req.route.path === '/rss/:page/')) {
+ return res.redirect(config.paths().subdir + '/rss/');
+ }
+
//needs refact for multi user to not use first user as default
- when.settle([
+ return when.settle([
api.users.read({id : 1}),
api.settings.read('title'),
- api.settings.read('description')
+ api.settings.read('description'),
+ api.settings.read('permalinks')
]).then(function (result) {
var user = result[0].value,
title = result[1].value.value,
- description = result[2].value.value;
+ description = result[2].value.value,
+ permalinks = result[3].value,
+ siteUrl = config.paths.urlFor('home', null, true),
+ feedUrl = config.paths.urlFor('rss', null, true);
feed = new RSS({
title: title,
description: description,
generator: 'Ghost v' + res.locals.version,
author: user ? user.name : null,
- feed_url: url.resolve(siteUrl, '/rss/'),
+ feed_url: feedUrl,
site_url: siteUrl,
ttl: '60'
});
- // No negative pages
- if (isNaN(pageParam) || pageParam < 1) {
- return res.redirect(config.paths().subdir + '/rss/');
- }
-
- if (pageParam === 1 && req.route.path === config.paths().subdir + '/rss/:page/') {
- return res.redirect(config.paths().subdir + '/rss/');
- }
-
- api.posts.browse({page: pageParam}).then(function (page) {
+ return api.posts.browse({page: pageParam}).then(function (page) {
var maxPage = page.pages,
feedItems = [];
@@ -187,37 +182,35 @@ frontendControllers = {
filters.doFilter('prePostsRender', page.posts).then(function (posts) {
posts.forEach(function (post) {
- var deferred = when.defer();
- post.url = coreHelpers.url;
- post.url().then(function (postUrl) {
- var item = {
- title: _.escape(post.title),
- guid: post.uuid,
- url: siteUrl + postUrl,
- date: post.published_at,
- categories: _.pluck(post.tags, 'name')
- },
- content = post.html;
+ var deferred = when.defer(),
+ item = {
+ title: _.escape(post.title),
+ guid: post.uuid,
+ url: config.paths.urlFor('post', {post: post, permalinks: permalinks}, true),
+ date: post.published_at,
+ categories: _.pluck(post.tags, 'name')
+ },
+ content = post.html;
- //set img src to absolute url
- content = content.replace(/src=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
- /*jslint unparam:true*/
- p1 = url.resolve(siteUrl, p1);
- return "src='" + p1 + "' ";
- });
- //set a href to absolute url
- content = content.replace(/href=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
- /*jslint unparam:true*/
- p1 = url.resolve(siteUrl, p1);
- return "href='" + p1 + "' ";
- });
- item.description = content;
- feed.item(item);
- deferred.resolve();
+ //set img src to absolute url
+ content = content.replace(/src=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
+ /*jslint unparam:true*/
+ p1 = url.resolve(siteUrl, p1);
+ return "src='" + p1 + "' ";
});
+ //set a href to absolute url
+ content = content.replace(/href=["|'|\s]?([\w\/\?\$\.\+\-;%:@&=,_]+)["|'|\s]?/gi, function (match, p1) {
+ /*jslint unparam:true*/
+ p1 = url.resolve(siteUrl, p1);
+ return "href='" + p1 + "' ";
+ });
+ item.description = content;
+ feed.item(item);
+ deferred.resolve();
feedItems.push(deferred.promise);
});
});
+
when.all(feedItems).then(function () {
res.set('Content-Type', 'text/xml');
res.send(feed.xml());
diff --git a/core/server/data/export/index.js b/core/server/data/export/index.js
index a28c2867cd..9a28634031 100644
--- a/core/server/data/export/index.js
+++ b/core/server/data/export/index.js
@@ -2,7 +2,7 @@ var when = require('when'),
_ = require('underscore'),
migration = require('../migration'),
knex = require('../../models/base').knex,
- schema = require('../schema'),
+ schema = require('../schema').tables,
excludedTables = ['sessions'],
exporter;
diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js
index 2065878392..ff1daa3b89 100644
--- a/core/server/data/migration/index.js
+++ b/core/server/data/migration/index.js
@@ -8,7 +8,7 @@ var _ = require('underscore'),
defaultSettings = require('../default-settings'),
Settings = require('../../models/settings').Settings,
fixtures = require('../fixtures'),
- schema = require('../schema'),
+ schema = require('../schema').tables,
initialVersion = '000',
schemaTables = _.keys(schema),
diff --git a/core/server/data/schema.js b/core/server/data/schema.js
index 26962aaa84..bf90ee90ff 100644
--- a/core/server/data/schema.js
+++ b/core/server/data/schema.js
@@ -118,4 +118,12 @@ var db = {
}
};
-module.exports = db;
\ No newline at end of file
+function isPost(jsonData) {
+ return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown')
+ && jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug');
+}
+
+module.exports.tables = db;
+module.exports.checks = {
+ isPost: isPost
+};
\ No newline at end of file
diff --git a/core/server/helpers/index.js b/core/server/helpers/index.js
index 0c54761bc8..9b9b297d37 100644
--- a/core/server/helpers/index.js
+++ b/core/server/helpers/index.js
@@ -10,8 +10,8 @@ var downsize = require('downsize'),
config = require('../config'),
errors = require('../errorHandling'),
filters = require('../filters'),
- models = require('../models'),
template = require('./template'),
+ schema = require('../data/schema').checks,
assetTemplate = _.template('<%= source %>?v=<%= version %>'),
scriptTemplate = _.template(''),
@@ -80,7 +80,7 @@ coreHelpers.encode = function (context, str) {
//
coreHelpers.pageUrl = function (context, block) {
/*jslint unparam:true*/
- return context === 1 ? '/' : ('/page/' + context + '/');
+ return config.paths().subdir + (context === 1 ? '/' : ('/page/' + context + '/'));
};
// ### URL helper
@@ -93,36 +93,13 @@ coreHelpers.pageUrl = function (context, block) {
// i.e. If inside a post context will return post permalink
// absolute flag outputs absolute URL, else URL is relative
coreHelpers.url = function (options) {
- var output = '',
- self = this,
- tags = {
- year: function () { return moment(self.published_at).format('YYYY'); },
- month: function () { return moment(self.published_at).format('MM'); },
- day: function () { return moment(self.published_at).format('DD'); },
- slug: function () { return self.slug; },
- id: function () { return self.id; }
- },
- isAbsolute = options && options.hash.absolute;
- return api.settings.read('permalinks').then(function (permalinks) {
- if (isAbsolute) {
- output += config().url.replace(/\/$/, '');
- } else {
- output += config.paths().subdir;
- }
- if (models.isPost(self)) {
- if (self.page === 1) {
- output += '/:slug/';
- } else {
- output += permalinks.value;
- }
- output = output.replace(/(:[a-z]+)/g, function (match) {
- if (_.has(tags, match.substr(1))) {
- return tags[match.substr(1)]();
- }
- });
- }
- return output;
- });
+ var absolute = options && options.hash.absolute;
+
+ if (schema.isPost(this)) {
+ return config.paths.urlForPost(api.settings, this, absolute);
+ }
+
+ return when(config.paths.urlFor(this, absolute));
};
// ### Asset helper
@@ -317,9 +294,9 @@ coreHelpers.body_class = function (options) {
tags = this.post && this.post.tags ? this.post.tags : this.tags || [],
page = this.post && this.post.page ? this.post.page : this.page || false;
- if (_.isString(this.ghostRoot) && this.ghostRoot.match(/\/page/)) {
+ if (_.isString(this.relativeUrl) && this.relativeUrl.match(/\/page/)) {
classes.push('archive-template');
- } else if (!this.ghostRoot || this.ghostRoot === '/' || this.ghostRoot === '') {
+ } else if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '') {
classes.push('home-template');
} else {
classes.push('post-template');
@@ -366,7 +343,8 @@ coreHelpers.post_class = function (options) {
coreHelpers.ghost_head = function (options) {
/*jslint unparam:true*/
- var blog = config.theme(),
+ var self = this,
+ blog = config.theme(),
head = [],
majorMinor = /^(\d+\.)?(\d+)/,
trimmedVersion = this.version;
@@ -376,13 +354,13 @@ coreHelpers.ghost_head = function (options) {
head.push('