From af6137248dcfd6bb4eb74858d1a0bc8a5721ef96 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Fri, 3 Jan 2014 00:37:21 +0000 Subject: [PATCH] New URL helper - URL consistency fixes fixes #1765 fixes #1811 issue #1833 New UrlFor functions - moved body of url helper to config.path.urlFor, which can generate a URL for various scenarios - urlFor can take a string (name) or object (relativeUrl: '/') as the first argument - this is the first step towards issue #1833 - also added config.path.urlForPost which is async and handles getting permalink setting - frontend controller, ghost_head helper, cache invalidation all now use urlFor or urlForPost all urls should be correct and consistent URL Consistency Improvements - refactored invalidateCache into cacheInvalidationHeader which returns a promise so that url can be generated properly by urlForPost - moved isPost from models to schema, and refactored schema to have a tables object - deleted posts now return the whole object, not just id and slug, ensuring cache invalidation header can be set on delete - frontend controller rss and archive page redirects work properly with subdirectory - removes {{url}} helper from admin and client, and replaced with adminUrl helper which also uses urlFor - in res.locals ghostRoot becomes relativeUrl, and path is removed --- core/client/helpers/index.js | 4 +- core/client/tpl/login.hbs | 2 +- core/client/tpl/preview.hbs | 2 +- core/server/api/db.js | 2 +- core/server/api/index.js | 21 +- core/server/api/posts.js | 4 +- core/server/config/paths.js | 132 ++++- core/server/controllers/frontend.js | 101 ++-- core/server/data/export/index.js | 2 +- core/server/data/migration/index.js | 2 +- core/server/data/schema.js | 10 +- core/server/helpers/index.js | 79 ++- core/server/middleware/index.js | 5 +- core/server/models/index.js | 4 - core/server/views/content.hbs | 2 +- core/server/views/debug.hbs | 4 +- core/server/views/partials/navbar.hbs | 10 +- core/test/functional/api/posts_test.js | 549 +++++++++++-------- core/test/functional/frontend/feed_test.js | 4 +- core/test/functional/frontend/home_test.js | 58 +- core/test/functional/frontend/post_test.js | 9 +- core/test/functional/routes/frontend_test.js | 1 - core/test/unit/config_spec.js | 244 ++++++++- core/test/unit/frontend_spec.js | 216 +++++++- core/test/unit/server_helpers_index_spec.js | 199 ++++++- 25 files changed, 1269 insertions(+), 397 deletions(-) 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 @@
- Forgotten password? + Forgotten password?
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(''); head.push(''); + + _.escape(blog.title) + '" href="' + config.paths.urlFor('rss') + '">'); - if (this.ghostRoot) { - head.push(''); - } + return coreHelpers.url.call(self, {hash: {absolute: true}}).then(function (url) { + head.push(''); - return filters.doFilter('ghost_head', head).then(function (head) { + return filters.doFilter('ghost_head', head); + }).then(function (head) { var headString = _.reduce(head, function (memo, item) { return memo + '\n' + item; }, ''); return new hbs.handlebars.SafeString(headString.trim()); }); @@ -408,8 +386,8 @@ coreHelpers.meta_title = function (options) { var title, blog; - if (_.isString(this.ghostRoot)) { - if (!this.ghostRoot || this.ghostRoot === '/' || this.ghostRoot === '' || this.ghostRoot.match(/\/page/)) { + if (_.isString(this.relativeUrl)) { + if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '' || this.relativeUrl.match(/\/page/)) { blog = config.theme(); title = blog.title; } else { @@ -428,8 +406,8 @@ coreHelpers.meta_description = function (options) { var description, blog; - if (_.isString(this.ghostRoot)) { - if (!this.ghostRoot || this.ghostRoot === '/' || this.ghostRoot === '' || this.ghostRoot.match(/\/page/)) { + if (_.isString(this.relativeUrl)) { + if (!this.relativeUrl || this.relativeUrl === '/' || this.relativeUrl === '' || this.relativeUrl.match(/\/page/)) { blog = config.theme(); description = blog.description; } else { @@ -572,6 +550,16 @@ coreHelpers.helperMissing = function (arg) { errors.logError('Missing helper: "' + arg + '"'); }; +// ## Admin URL helper +// uses urlFor to generate a URL for either the admin or the frontend. +coreHelpers.adminUrl = function (options) { + var absolute = options && options.hash && options.hash.absolute, + // Ghost isn't a named route as currently it violates the must start-and-end with slash rule + context = !options || !options.hash || !options.hash.frontend ? {relativeUrl: '/ghost'} : 'home'; + + return config.paths.urlFor(context, absolute); +}; + // Register an async handlebars helper for a given handlebars instance function registerAsyncHelper(hbs, name, fn) { hbs.registerAsyncHelper(name, function (options, cb) { @@ -607,7 +595,6 @@ function registerAsyncAdminHelper(name, fn) { } - registerHelpers = function (adminHbs, assetHash) { // Expose hbs instance for admin @@ -662,7 +649,7 @@ registerHelpers = function (adminHbs, assetHash) { registerAdminHelper('fileStorage', coreHelpers.fileStorage); - registerAsyncAdminHelper('url', coreHelpers.url); + registerAdminHelper('adminUrl', coreHelpers.adminUrl); }; diff --git a/core/server/middleware/index.js b/core/server/middleware/index.js index fdc748948d..506876ec55 100644 --- a/core/server/middleware/index.js +++ b/core/server/middleware/index.js @@ -33,9 +33,8 @@ function ghostLocals(req, res, next) { // Make sure we have a locals value. res.locals = res.locals || {}; res.locals.version = packageInfo.version; - res.locals.path = req.path; - // Strip off the subdir part of the path - res.locals.ghostRoot = req.path.replace(config.paths().subdir, ''); + // relative path from the URL, not including subdir + res.locals.relativeUrl = req.path.replace(config.paths().subdir, ''); if (res.isAdmin) { res.locals.csrfToken = req.csrfToken(); diff --git a/core/server/models/index.js b/core/server/models/index.js index a01f51af1b..f5f1eefc45 100644 --- a/core/server/models/index.js +++ b/core/server/models/index.js @@ -35,9 +35,5 @@ module.exports = { }); }); }); - }, - isPost: function (jsonData) { - return jsonData.hasOwnProperty('html') && jsonData.hasOwnProperty('markdown') - && jsonData.hasOwnProperty('title') && jsonData.hasOwnProperty('slug'); } }; diff --git a/core/server/views/content.hbs b/core/server/views/content.hbs index 0969717ba8..502e5f95da 100644 --- a/core/server/views/content.hbs +++ b/core/server/views/content.hbs @@ -5,7 +5,7 @@
All Posts
- +
    diff --git a/core/server/views/debug.hbs b/core/server/views/debug.hbs index b5b1b6af11..1fd7b9b824 100644 --- a/core/server/views/debug.hbs +++ b/core/server/views/debug.hbs @@ -20,12 +20,12 @@
    - Export + Export

    Export the blog settings and data.

    -
    +
    diff --git a/core/server/views/partials/navbar.hbs b/core/server/views/partials/navbar.hbs index 925f422909..ee0e26bad9 100644 --- a/core/server/views/partials/navbar.hbs +++ b/core/server/views/partials/navbar.hbs @@ -1,9 +1,11 @@