mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
✨Dynamic Routing Beta (#9596)
refs #9601 ### Dynamic Routing This is the beta version of dynamic routing. - we had a initial implementation of "channels" available in the codebase - we have removed and moved this implementation - there is now a centralised place for dynamic routing - server/services/routing - each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts - keep as much as possible logic of routing helpers, middlewares and controllers - ensure test coverage - connect all the things together - yaml file + validation - routing + routers - url service - sitemaps - url access - deeper implementation of yaml validations - e.g. hard require slashes - ensure routing hierarchy/order - e.g. you enable the subscriber app - you have a custom static page, which lives under the same slug /subscribe - static pages are stronger than apps - e.g. the first collection owns the post it has filtered - a post cannot live in two collections - ensure apps are still working and hook into the routers layer (or better said: and register in the routing service) - put as much as possible comments to the code base for better understanding - ensure a clean debug log - ensure we can unmount routes - e.g. you have a collection permalink of /:slug/ represented by {globals.permalink} - and you change the permalink in the admin to dated permalink - the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/ - unmount without server restart, yey - ensure we are backwards compatible - e.g. render home.hbs for collection index if collection route is / - ensure you can access your configured permalink from the settings table with {globals.permalink} ### Render 503 if url service did not finish - return 503 if the url service has not finished generating the resource urls ### Rewrite sitemaps - we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime - we generate all urls on bootstrap - the sitemaps service will consume created resource and router urls - these urls will be shown on the xml pages - we listen on url events - we listen on router events - we no longer have to fetch the resources, which is nice - the urlservice pre-fetches resources and emits their urls - the urlservice is the only component who knows which urls are valid - i made some ES6 adaptions - we keep the caching logic -> only regenerate xml if there is a change - updated tests - checked test coverage (100%) ### Re-work usage of Url utility - replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId` - only for resources e.g. post, author, tag - this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime - adapt url utility - adapt tests
This commit is contained in:
parent
9b704f1691
commit
b392d1925a
161 changed files with 8119 additions and 7742 deletions
|
@ -11,7 +11,7 @@ routes:
|
|||
|
||||
collections:
|
||||
/:
|
||||
route: '{globals.permalinks}'
|
||||
permalink: '{globals.permalinks}'
|
||||
template:
|
||||
- home
|
||||
- index
|
||||
|
|
|
@ -107,7 +107,7 @@ db = {
|
|||
*/
|
||||
deleteAllContent: function deleteAllContent(options) {
|
||||
var tasks,
|
||||
queryOpts = {columns: 'id', context: {internal: true}};
|
||||
queryOpts = {columns: 'id', context: {internal: true}, destroyAll: true};
|
||||
|
||||
options = options || {};
|
||||
|
||||
|
|
|
@ -4,9 +4,8 @@ var path = require('path'),
|
|||
|
||||
// Dirty requires
|
||||
common = require('../../../lib/common'),
|
||||
postLookup = require('../../../controllers/frontend/post-lookup'),
|
||||
renderer = require('../../../controllers/frontend/renderer'),
|
||||
|
||||
urlService = require('../../../services/url'),
|
||||
helpers = require('../../../services/routing/helpers'),
|
||||
templateName = 'amp';
|
||||
|
||||
function _renderer(req, res, next) {
|
||||
|
@ -28,7 +27,7 @@ function _renderer(req, res, next) {
|
|||
}
|
||||
|
||||
// Render Call
|
||||
return renderer(req, res, data);
|
||||
return helpers.renderer(req, res, data);
|
||||
}
|
||||
|
||||
// This here is a controller.
|
||||
|
@ -36,7 +35,38 @@ function _renderer(req, res, next) {
|
|||
function getPostData(req, res, next) {
|
||||
req.body = req.body || {};
|
||||
|
||||
postLookup(res.locals.relativeUrl)
|
||||
const urlWithSubdirectoryWithoutAmp = req.originalUrl.match(/(.*?\/)amp/)[1];
|
||||
const urlWithoutSubdirectoryWithoutAmp = res.locals.relativeUrl.match(/(.*?\/)amp/)[1];
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
*
|
||||
* We have to figure out the target permalink, otherwise it would be possible to serve a post
|
||||
* which lives in two collections.
|
||||
*
|
||||
* @TODO:
|
||||
*
|
||||
* This is not optimal and caused by the fact how apps currently work. But apps weren't designed
|
||||
* for dynamic routing.
|
||||
*
|
||||
* I think if the responsible, target router would first take care fetching/determining the post, the
|
||||
* request could then be forwarded to this app. Then we don't have to:
|
||||
*
|
||||
* 1. care about fetching the post
|
||||
* 2. care about if the post can be served
|
||||
* 3. then this app would act like an extension
|
||||
*
|
||||
* The challenge is to design different types of apps e.g. extensions of routers, standalone pages etc.
|
||||
*/
|
||||
const permalinks = urlService.getPermalinkByUrl(urlWithSubdirectoryWithoutAmp, {withUrlOptions: true});
|
||||
|
||||
if (!permalinks) {
|
||||
return next(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.errors.pageNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
helpers.postLookup(urlWithoutSubdirectoryWithoutAmp, {permalinks: permalinks})
|
||||
.then(function handleResult(result) {
|
||||
if (result && result.post) {
|
||||
req.body.post = result.post;
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
var path = require('path'),
|
||||
express = require('express'),
|
||||
middleware = require('./middleware'),
|
||||
bodyParser = require('body-parser'),
|
||||
renderer = require('../../../controllers/frontend/renderer'),
|
||||
brute = require('../../../web/middleware/brute'),
|
||||
|
||||
const path = require('path'),
|
||||
express = require('express'),
|
||||
middleware = require('./middleware'),
|
||||
bodyParser = require('body-parser'),
|
||||
routing = require('../../../services/routing'),
|
||||
brute = require('../../../web/middleware/brute'),
|
||||
templateName = 'private',
|
||||
|
||||
privateRouter = express.Router();
|
||||
|
||||
function _renderer(req, res) {
|
||||
|
@ -27,7 +25,7 @@ function _renderer(req, res) {
|
|||
}
|
||||
|
||||
// Render Call
|
||||
return renderer(req, res, data);
|
||||
return routing.helpers.renderer(req, res, data);
|
||||
}
|
||||
|
||||
// password-protected frontend route
|
||||
|
|
|
@ -5,7 +5,7 @@ var _ = require('lodash'),
|
|||
// (Less) dirty requires
|
||||
proxy = require('../../../../helpers/proxy'),
|
||||
templates = proxy.templates,
|
||||
url = proxy.url,
|
||||
urlService = proxy.urlService,
|
||||
SafeString = proxy.SafeString,
|
||||
|
||||
params = ['error', 'success', 'email'],
|
||||
|
@ -39,7 +39,7 @@ module.exports = function subscribe_form(options) { // eslint-disable-line camel
|
|||
var root = options.data.root,
|
||||
data = _.merge({}, options.hash, _.pick(root, params), {
|
||||
// routeKeywords.subscribe: 'subscribe'
|
||||
action: url.urlJoin('/', url.getSubdir(), 'subscribe/'),
|
||||
action: urlService.utils.urlJoin('/', urlService.utils.getSubdir(), 'subscribe/'),
|
||||
script: new SafeString(subscribeScript),
|
||||
hidden: new SafeString(
|
||||
makeHidden('confirm') +
|
||||
|
|
|
@ -7,9 +7,9 @@ var path = require('path'),
|
|||
// Dirty requires
|
||||
api = require('../../../api'),
|
||||
common = require('../../../lib/common'),
|
||||
urlService = require('../../../services/url'),
|
||||
validator = require('../../../data/validation').validator,
|
||||
postLookup = require('../../../controllers/frontend/post-lookup'),
|
||||
renderer = require('../../../controllers/frontend/renderer'),
|
||||
routing = require('../../../services/routing'),
|
||||
|
||||
templateName = 'subscribe';
|
||||
|
||||
|
@ -27,7 +27,7 @@ function _renderer(req, res) {
|
|||
var data = req.body;
|
||||
|
||||
// Render Call
|
||||
return renderer(req, res, data);
|
||||
return routing.helpers.renderer(req, res, data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,24 +63,17 @@ function santizeUrl(url) {
|
|||
function handleSource(req, res, next) {
|
||||
req.body.subscribed_url = santizeUrl(req.body.location);
|
||||
req.body.subscribed_referrer = santizeUrl(req.body.referrer);
|
||||
|
||||
delete req.body.location;
|
||||
delete req.body.referrer;
|
||||
|
||||
postLookup(req.body.subscribed_url)
|
||||
.then(function (result) {
|
||||
if (result && result.post) {
|
||||
req.body.post_id = result.post.id;
|
||||
}
|
||||
const resource = urlService.getResource(urlService.utils.absoluteToRelative(req.body.subscribed_url));
|
||||
|
||||
next();
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (err instanceof common.errors.NotFoundError) {
|
||||
return next();
|
||||
}
|
||||
if (resource) {
|
||||
req.body.post_id = resource.data.id;
|
||||
}
|
||||
|
||||
next(err);
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
function storeSubscriber(req, res, next) {
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
var _ = require('lodash'),
|
||||
common = require('../lib/common'),
|
||||
security = require('../lib/security'),
|
||||
filters = require('../filters'),
|
||||
handleError = require('./frontend/error'),
|
||||
fetchData = require('./frontend/fetch-data'),
|
||||
setRequestIsSecure = require('./frontend/secure'),
|
||||
renderChannel = require('./frontend/render-channel');
|
||||
|
||||
// This here is a controller.
|
||||
// The "route" is handled in services/channels/router.js
|
||||
// There's both a top-level channelS router, and an individual channel one
|
||||
module.exports = function channelController(req, res, next) {
|
||||
// Parse the parameters we need from the URL
|
||||
var pageParam = req.params.page !== undefined ? req.params.page : 1,
|
||||
slugParam = req.params.slug ? security.string.safe(req.params.slug) : undefined;
|
||||
|
||||
// @TODO: fix this, we shouldn't change the channel object!
|
||||
// Set page on postOptions for the query made later
|
||||
res.locals.channel.postOptions.page = pageParam;
|
||||
res.locals.channel.slugParam = slugParam;
|
||||
|
||||
// Call fetchData to get everything we need from the API
|
||||
return fetchData(res.locals.channel).then(function handleResult(result) {
|
||||
// If page is greater than number of pages we have, go straight to 404
|
||||
if (pageParam > result.meta.pagination.pages) {
|
||||
return next(new common.errors.NotFoundError({message: common.i18n.t('errors.errors.pageNotFound')}));
|
||||
}
|
||||
|
||||
// Format data 1
|
||||
// @TODO: figure out if this can be removed, it's supposed to ensure that absolutely URLs get generated
|
||||
// correctly for the various objects, but I believe it doesn't work and a different approach is needed.
|
||||
setRequestIsSecure(req, result.posts);
|
||||
_.each(result.data, function (data) {
|
||||
setRequestIsSecure(req, data);
|
||||
});
|
||||
|
||||
// @TODO: properly design these filters
|
||||
filters.doFilter('prePostsRender', result.posts, res.locals)
|
||||
.then(function (posts) {
|
||||
result.posts = posts;
|
||||
return result;
|
||||
})
|
||||
.then(renderChannel(req, res));
|
||||
}).catch(handleError(next));
|
||||
};
|
|
@ -34,8 +34,20 @@ module.exports = function entryController(req, res, next) {
|
|||
return urlService.utils.redirectToAdmin(302, res, '/editor/' + post.id);
|
||||
}
|
||||
|
||||
// CASE: permalink is not valid anymore, we redirect him permanently to the correct one
|
||||
if (post.url !== req.path) {
|
||||
//
|
||||
/**
|
||||
* CASE: Permalink is not valid anymore, we redirect him permanently to the correct one
|
||||
* This usually only happens if you switch to date permalinks or if you have date permalinks
|
||||
* enabled and the published date changes.
|
||||
*
|
||||
* @NOTE
|
||||
*
|
||||
* The resource url (post.url) always contains the subdirectory. This was different before dynamic routing.
|
||||
* Because with dynamic routing we have a service which knows where a resource lives - and this includes the
|
||||
* subdirectory. Otherwise every time we use a resource url, we would need to take care of the subdirectory.
|
||||
* That's why we have to use the original url, which contains the sub-directory.
|
||||
*/
|
||||
if (post.url !== req.originalUrl) {
|
||||
return urlService.utils.redirect301(res, post.url);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
/**
|
||||
* # Fetch Data
|
||||
* Dynamically build and execute queries on the API for channels
|
||||
*/
|
||||
var api = require('../../api'),
|
||||
_ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
themes = require('../../services/themes'),
|
||||
queryDefaults,
|
||||
defaultPostQuery = {};
|
||||
|
||||
// The default settings for a default post query
|
||||
queryDefaults = {
|
||||
type: 'browse',
|
||||
resource: 'posts',
|
||||
options: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Default post query needs to always include author, authors & tags
|
||||
*
|
||||
* @deprecated: `author`, will be removed in Ghost 2.0
|
||||
*/
|
||||
_.extend(defaultPostQuery, queryDefaults, {
|
||||
options: {
|
||||
include: 'author,authors,tags'
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ## Fetch Posts Per page
|
||||
* Grab the postsPerPage setting
|
||||
*
|
||||
* @param {Object} options
|
||||
* @returns {Object} postOptions
|
||||
*/
|
||||
function fetchPostsPerPage(options) {
|
||||
options = options || {};
|
||||
|
||||
var postsPerPage = parseInt(themes.getActive().config('posts_per_page'));
|
||||
|
||||
// No negative posts per page, must be number
|
||||
if (!isNaN(postsPerPage) && postsPerPage > 0) {
|
||||
options.limit = postsPerPage;
|
||||
}
|
||||
|
||||
// Ensure the options key is present, so this can be merged with other options
|
||||
return {options: options};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef query
|
||||
* @
|
||||
*/
|
||||
|
||||
/**
|
||||
* ## Process Query
|
||||
* Takes a 'query' object, ensures that type, resource and options are set
|
||||
* Replaces occurrences of `%s` in options with slugParam
|
||||
* Converts the query config to a promise for the result
|
||||
*
|
||||
* @param {{type: String, resource: String, options: Object}} query
|
||||
* @param {String} slugParam
|
||||
* @returns {Promise} promise for an API call
|
||||
*/
|
||||
function processQuery(query, slugParam) {
|
||||
query = _.cloneDeep(query);
|
||||
|
||||
// Ensure that all the properties are filled out
|
||||
_.defaultsDeep(query, queryDefaults);
|
||||
|
||||
// Replace any slugs
|
||||
_.each(query.options, function (option, name) {
|
||||
query.options[name] = _.isString(option) ? option.replace(/%s/g, slugParam) : option;
|
||||
});
|
||||
|
||||
// Return a promise for the api query
|
||||
return api[query.resource][query.type](query.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Fetch Data
|
||||
* Calls out to get posts per page, builds the final posts query & builds any additional queries
|
||||
* Wraps the queries using Promise.props to ensure it gets named responses
|
||||
* Does a first round of formatting on the response, and returns
|
||||
*
|
||||
* @param {Object} channelOptions
|
||||
* @returns {Promise} response
|
||||
*/
|
||||
function fetchData(channelOptions) {
|
||||
// @TODO improve this further
|
||||
var pageOptions = channelOptions.isRSS ? {options: channelOptions.postOptions} : fetchPostsPerPage(channelOptions.postOptions),
|
||||
postQuery,
|
||||
props = {};
|
||||
|
||||
// All channels must have a posts query, use the default if not provided
|
||||
postQuery = _.defaultsDeep({}, pageOptions, defaultPostQuery);
|
||||
props.posts = processQuery(postQuery, channelOptions.slugParam);
|
||||
|
||||
_.each(channelOptions.data, function (query, name) {
|
||||
props[name] = processQuery(query, channelOptions.slugParam);
|
||||
});
|
||||
|
||||
return Promise.props(props).then(function formatResponse(results) {
|
||||
var response = _.cloneDeep(results.posts);
|
||||
delete results.posts;
|
||||
|
||||
// process any remaining data
|
||||
if (!_.isEmpty(results)) {
|
||||
response.data = {};
|
||||
|
||||
_.each(results, function (result, name) {
|
||||
if (channelOptions.data[name].type === 'browse') {
|
||||
response.data[name] = result;
|
||||
} else {
|
||||
response.data[name] = result[channelOptions.data[name].resource];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fetchData;
|
|
@ -1,73 +0,0 @@
|
|||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
url = require('url'),
|
||||
routeMatch = require('path-match')(),
|
||||
api = require('../../api'),
|
||||
settingsCache = require('../../services/settings/cache'),
|
||||
optionsFormat = '/:options?';
|
||||
|
||||
function getOptionsFormat(linkStructure) {
|
||||
return linkStructure.replace(/\/$/, '') + optionsFormat;
|
||||
}
|
||||
|
||||
function postLookup(postUrl) {
|
||||
var postPath = url.parse(postUrl).path,
|
||||
postPermalink = settingsCache.get('permalinks'),
|
||||
pagePermalink = '/:slug/',
|
||||
isEditURL = false,
|
||||
matchFuncPost,
|
||||
matchFuncPage,
|
||||
postParams,
|
||||
params;
|
||||
|
||||
// Convert saved permalink into a path-match function
|
||||
matchFuncPost = routeMatch(getOptionsFormat(postPermalink));
|
||||
matchFuncPage = routeMatch(getOptionsFormat(pagePermalink));
|
||||
|
||||
postParams = matchFuncPost(postPath);
|
||||
|
||||
// Check if the path matches the permalink structure.
|
||||
// If there are no matches found, test to see if this is a page instead
|
||||
params = postParams || matchFuncPage(postPath);
|
||||
|
||||
// if there are no matches for either then return empty
|
||||
if (params === false) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If params contains options, and it is equal to 'edit', this is an edit URL
|
||||
if (params.options && params.options.toLowerCase() === 'edit') {
|
||||
isEditURL = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query database to find post.
|
||||
*
|
||||
* @deprecated: `author`, will be removed in Ghost 2.0
|
||||
*/
|
||||
return api.posts.read(_.extend(_.pick(params, 'slug', 'id'), {include: 'author,authors,tags'})).then(function then(result) {
|
||||
var post = result.posts[0];
|
||||
|
||||
if (!post) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// CASE: we originally couldn't match the post based on date permalink and we tried to check if its a page
|
||||
if (!post.page && !postParams) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// CASE: we only support /:slug format for pages
|
||||
if (post.page && matchFuncPage(postPath) === false) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return {
|
||||
post: post,
|
||||
isEditURL: isEditURL,
|
||||
isUnknownOption: isEditURL ? false : params.options ? true : false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = postLookup;
|
|
@ -1,16 +0,0 @@
|
|||
var debug = require('ghost-ignition').debug('channels:render'),
|
||||
formatResponse = require('./format-response'),
|
||||
renderer = require('./renderer');
|
||||
|
||||
module.exports = function renderChannel(req, res) {
|
||||
debug('renderChannel called');
|
||||
return function renderChannel(result) {
|
||||
// Renderer begin
|
||||
// Format data 2
|
||||
// Do final data formatting and then render
|
||||
var data = formatResponse.channel(result);
|
||||
|
||||
// Render Call
|
||||
return renderer(req, res, data);
|
||||
};
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
module.exports = {
|
||||
preview: require('./preview'),
|
||||
entry: require('./entry')
|
||||
};
|
|
@ -37,7 +37,7 @@ module.exports = function previewController(req, res, next) {
|
|||
}
|
||||
|
||||
if (post.status === 'published') {
|
||||
return urlService.utils.redirect301(res, urlService.utils.urlFor('post', {post: post}));
|
||||
return urlService.utils.redirect301(res, urlService.getUrlByResourceId(post.id));
|
||||
}
|
||||
|
||||
setRequestIsSecure(req, post);
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
var _ = require('lodash'),
|
||||
url = require('url'),
|
||||
common = require('../lib/common'),
|
||||
security = require('../lib/security'),
|
||||
settingsCache = require('../services/settings/cache'),
|
||||
|
||||
// Slightly less ugly temporary hack for location of things
|
||||
fetchData = require('./frontend/fetch-data'),
|
||||
handleError = require('./frontend/error'),
|
||||
|
||||
rssService = require('../services/rss'),
|
||||
generate;
|
||||
|
||||
// @TODO: is this the right logic? Where should this live?!
|
||||
function getBaseUrlForRSSReq(originalUrl, pageParam) {
|
||||
return url.parse(originalUrl).pathname.replace(new RegExp('/' + pageParam + '/$'), '/');
|
||||
}
|
||||
|
||||
// @TODO: is this really correct? Should we be using meta data title?
|
||||
function getTitle(relatedData) {
|
||||
relatedData = relatedData || {};
|
||||
var titleStart = _.get(relatedData, 'author[0].name') || _.get(relatedData, 'tag[0].name') || '';
|
||||
|
||||
titleStart += titleStart ? ' - ' : '';
|
||||
return titleStart + settingsCache.get('title');
|
||||
}
|
||||
|
||||
// @TODO: merge this with the rest of the data processing for RSS
|
||||
// @TODO: swap the fetchData call + duplicate code from channels with something DRY
|
||||
function getData(channelOpts) {
|
||||
channelOpts.data = channelOpts.data || {};
|
||||
|
||||
return fetchData(channelOpts).then(function formatResult(result) {
|
||||
var response = _.pick(result, ['posts', 'meta']);
|
||||
|
||||
response.title = getTitle(result.data);
|
||||
response.description = settingsCache.get('description');
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
// This here is a controller.
|
||||
// The "route" is handled in services/channels/router.js
|
||||
// We can only generate RSS for channels, so that sorta makes sense, but the location is rubbish
|
||||
// @TODO finish refactoring this - it's now a controller
|
||||
generate = function generate(req, res, next) {
|
||||
// Parse the parameters we need from the URL
|
||||
var pageParam = req.params.page !== undefined ? req.params.page : 1,
|
||||
slugParam = req.params.slug ? security.string.safe(req.params.slug) : undefined,
|
||||
// Base URL needs to be the URL for the feed without pagination:
|
||||
baseUrl = getBaseUrlForRSSReq(req.originalUrl, pageParam);
|
||||
|
||||
// @TODO: fix this, we shouldn't change the channel object!
|
||||
// Set page on postOptions for the query made later
|
||||
res.locals.channel.postOptions.page = pageParam;
|
||||
res.locals.channel.slugParam = slugParam;
|
||||
|
||||
return getData(res.locals.channel).then(function handleResult(data) {
|
||||
// If page is greater than number of pages we have, go straight to 404
|
||||
if (pageParam > data.meta.pagination.pages) {
|
||||
return next(new common.errors.NotFoundError({message: common.i18n.t('errors.errors.pageNotFound')}));
|
||||
}
|
||||
|
||||
// Render call - to a special RSS renderer
|
||||
return rssService.render(res, baseUrl, data);
|
||||
}).catch(handleError(next));
|
||||
};
|
||||
|
||||
module.exports = generate;
|
|
@ -6,12 +6,13 @@ function getAuthorUrl(data, absolute) {
|
|||
context = context === 'amp' ? 'post' : context;
|
||||
|
||||
if (data.author) {
|
||||
return urlService.utils.urlFor('author', {author: data.author}, absolute);
|
||||
return urlService.getUrlByResourceId(data.author.id, {absolute: absolute, secure: data.author.secure});
|
||||
}
|
||||
|
||||
if (data[context] && data[context].primary_author) {
|
||||
return urlService.utils.urlFor('author', {author: data[context].primary_author}, absolute);
|
||||
return urlService.getUrlByResourceId(data[context].primary_author.id, {absolute: absolute, secure: data[context].secure});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
var urlService = require('../../services/url');
|
||||
const routingService = require('../../services/routing');
|
||||
|
||||
/**
|
||||
* https://github.com/TryGhost/Team/issues/65#issuecomment-393622816
|
||||
*
|
||||
* For now we output only the default rss feed link. And this is the first collection.
|
||||
* If the first collection has rss disabled, we output nothing.
|
||||
*
|
||||
* @TODO: We are currently investigating this.
|
||||
*/
|
||||
function getRssUrl(data, absolute) {
|
||||
return urlService.utils.urlFor('rss', {secure: data.secure}, absolute);
|
||||
return routingService
|
||||
.registry
|
||||
.getFirstCollectionRouter()
|
||||
.getRssUrl({
|
||||
secure: data.secure,
|
||||
absolute: absolute
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = getRssUrl;
|
||||
|
|
|
@ -12,16 +12,8 @@ function sanitizeAmpUrl(url) {
|
|||
}
|
||||
|
||||
function getUrl(data, absolute) {
|
||||
if (schema.isPost(data)) {
|
||||
return urlService.utils.urlFor('post', {post: data, secure: data.secure}, absolute);
|
||||
}
|
||||
|
||||
if (schema.isTag(data)) {
|
||||
return urlService.utils.urlFor('tag', {tag: data, secure: data.secure}, absolute);
|
||||
}
|
||||
|
||||
if (schema.isUser(data)) {
|
||||
return urlService.utils.urlFor('author', {author: data, secure: data.secure}, absolute);
|
||||
if (schema.isPost(data) || schema.isTag(data) || schema.isUser(data)) {
|
||||
return urlService.getUrlByResourceId(data.id, {secure: data.secure, absolute: absolute});
|
||||
}
|
||||
|
||||
if (schema.isNav(data)) {
|
||||
|
|
|
@ -1,77 +1,28 @@
|
|||
var _ = require('lodash'),
|
||||
const _ = require('lodash'),
|
||||
xml = require('xml'),
|
||||
moment = require('moment'),
|
||||
Promise = require('bluebird'),
|
||||
path = require('path'),
|
||||
urlService = require('../../../services/url'),
|
||||
common = require('../../../lib/common'),
|
||||
localUtils = require('./utils'),
|
||||
CHANGE_FREQ = 'weekly',
|
||||
XMLNS_DECLS;
|
||||
CHANGE_FREQ = 'weekly';
|
||||
|
||||
// Sitemap specific xml namespace declarations that should not change
|
||||
XMLNS_DECLS = {
|
||||
const XMLNS_DECLS = {
|
||||
_attr: {
|
||||
xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9',
|
||||
'xmlns:image': 'http://www.google.com/schemas/sitemap-image/1.1'
|
||||
}
|
||||
};
|
||||
|
||||
function BaseSiteMapGenerator() {
|
||||
this.lastModified = 0;
|
||||
this.nodeLookup = {};
|
||||
this.nodeTimeLookup = {};
|
||||
this.siteMapContent = '';
|
||||
this.dataEvents = common.events;
|
||||
}
|
||||
class BaseSiteMapGenerator {
|
||||
constructor() {
|
||||
this.nodeLookup = {};
|
||||
this.nodeTimeLookup = {};
|
||||
this.siteMapContent = null;
|
||||
this.lastModified = 0;
|
||||
}
|
||||
|
||||
_.extend(BaseSiteMapGenerator.prototype, {
|
||||
init: function () {
|
||||
var self = this;
|
||||
return this.refreshAll().then(function () {
|
||||
return self.bindEvents();
|
||||
});
|
||||
},
|
||||
|
||||
bindEvents: _.noop,
|
||||
|
||||
getData: function () {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
|
||||
refreshAll: function () {
|
||||
var self = this;
|
||||
|
||||
// Load all data
|
||||
return this.getData().then(function (data) {
|
||||
// Generate SiteMap from data
|
||||
return self.generateXmlFromData(data);
|
||||
}).then(function (generatedXml) {
|
||||
self.siteMapContent = generatedXml;
|
||||
});
|
||||
},
|
||||
|
||||
generateXmlFromData: function (data) {
|
||||
// Create all the url elements in JSON
|
||||
var self = this,
|
||||
nodes;
|
||||
|
||||
nodes = _.reduce(data, function (nodeArr, datum) {
|
||||
var node = self.createUrlNodeFromDatum(datum);
|
||||
|
||||
if (node) {
|
||||
self.updateLastModified(datum);
|
||||
self.updateLookups(datum, node);
|
||||
nodeArr.push(node);
|
||||
}
|
||||
|
||||
return nodeArr;
|
||||
}, []);
|
||||
|
||||
return this.generateXmlFromNodes(nodes);
|
||||
},
|
||||
|
||||
generateXmlFromNodes: function () {
|
||||
generateXmlFromNodes() {
|
||||
var self = this,
|
||||
// Get a mapping of node to timestamp
|
||||
timedNodes = _.map(this.nodeLookup, function (node, id) {
|
||||
|
@ -93,70 +44,46 @@ _.extend(BaseSiteMapGenerator.prototype, {
|
|||
|
||||
// Return the xml
|
||||
return localUtils.getDeclarations() + xml(data);
|
||||
},
|
||||
}
|
||||
|
||||
updateXmlFromNodes: function (urlElements) {
|
||||
var content = this.generateXmlFromNodes(urlElements);
|
||||
|
||||
this.setSiteMapContent(content);
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
addOrUpdateUrl: function (model) {
|
||||
var datum = model.toJSON(),
|
||||
node = this.createUrlNodeFromDatum(datum);
|
||||
addUrl(url, datum) {
|
||||
const node = this.createUrlNodeFromDatum(url, datum);
|
||||
|
||||
if (node) {
|
||||
this.updateLastModified(datum);
|
||||
// TODO: Check if the node values changed, and if not don't regenerate
|
||||
this.updateLookups(datum, node);
|
||||
this.updateXmlFromNodes();
|
||||
// force regeneration of xml
|
||||
this.siteMapContent = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
removeUrl: function (model) {
|
||||
var datum = model.toJSON();
|
||||
// When the model is destroyed we need to fetch previousAttributes
|
||||
if (!datum.id) {
|
||||
datum = model.previousAttributes();
|
||||
}
|
||||
removeUrl(url, datum) {
|
||||
this.removeFromLookups(datum);
|
||||
|
||||
// force regeneration of xml
|
||||
this.siteMapContent = null;
|
||||
this.lastModified = Date.now();
|
||||
}
|
||||
|
||||
this.updateXmlFromNodes();
|
||||
},
|
||||
|
||||
validateDatum: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
getUrlForDatum: function () {
|
||||
return urlService.utils.urlFor('home', true);
|
||||
},
|
||||
|
||||
getUrlForImage: function (image) {
|
||||
return urlService.utils.urlFor('image', {image: image}, true);
|
||||
},
|
||||
|
||||
getPriorityForDatum: function () {
|
||||
getPriorityForDatum() {
|
||||
return 1.0;
|
||||
},
|
||||
}
|
||||
|
||||
getLastModifiedForDatum: function (datum) {
|
||||
getLastModifiedForDatum(datum) {
|
||||
return datum.updated_at || datum.published_at || datum.created_at;
|
||||
},
|
||||
}
|
||||
|
||||
createUrlNodeFromDatum: function (datum) {
|
||||
if (!this.validateDatum(datum)) {
|
||||
return false;
|
||||
updateLastModified(datum) {
|
||||
const lastModified = this.getLastModifiedForDatum(datum);
|
||||
|
||||
if (lastModified > this.lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
}
|
||||
|
||||
var url = this.getUrlForDatum(datum),
|
||||
priority = this.getPriorityForDatum(datum),
|
||||
node,
|
||||
imgNode;
|
||||
createUrlNodeFromDatum(url, datum) {
|
||||
const priority = this.getPriorityForDatum(datum);
|
||||
let node, imgNode;
|
||||
|
||||
node = {
|
||||
url: [
|
||||
|
@ -174,9 +101,9 @@ _.extend(BaseSiteMapGenerator.prototype, {
|
|||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
}
|
||||
|
||||
createImageNodeFromDatum: function (datum) {
|
||||
createImageNodeFromDatum(datum) {
|
||||
// Check for cover first because user has cover but the rest only have image
|
||||
var image = datum.cover_image || datum.profile_image || datum.feature_image,
|
||||
imageUrl,
|
||||
|
@ -187,7 +114,7 @@ _.extend(BaseSiteMapGenerator.prototype, {
|
|||
}
|
||||
|
||||
// Grab the image url
|
||||
imageUrl = this.getUrlForImage(image);
|
||||
imageUrl = urlService.utils.urlFor('image', {image: image}, true);
|
||||
|
||||
// Verify the url structure
|
||||
if (!this.validateImageUrl(imageUrl)) {
|
||||
|
@ -204,36 +131,37 @@ _.extend(BaseSiteMapGenerator.prototype, {
|
|||
return {
|
||||
'image:image': imageEl
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
validateImageUrl: function (imageUrl) {
|
||||
validateImageUrl(imageUrl) {
|
||||
return !!imageUrl;
|
||||
},
|
||||
}
|
||||
|
||||
setSiteMapContent: function (content) {
|
||||
this.siteMapContent = content;
|
||||
},
|
||||
|
||||
updateLastModified: function (datum) {
|
||||
var lastModified = this.getLastModifiedForDatum(datum);
|
||||
|
||||
if (lastModified > this.lastModified) {
|
||||
this.lastModified = lastModified;
|
||||
getXml() {
|
||||
if (this.siteMapContent) {
|
||||
return this.siteMapContent;
|
||||
}
|
||||
},
|
||||
|
||||
updateLookups: function (datum, node) {
|
||||
const content = this.generateXmlFromNodes();
|
||||
this.siteMapContent = content;
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
* The url service currently has no url update event.
|
||||
* It removes and adds the url. If the url service extends it's
|
||||
* feature set, we can detect if a node has changed.
|
||||
*/
|
||||
updateLookups(datum, node) {
|
||||
this.nodeLookup[datum.id] = node;
|
||||
this.nodeTimeLookup[datum.id] = this.getLastModifiedForDatum(datum);
|
||||
},
|
||||
|
||||
removeFromLookups: function (datum) {
|
||||
var lookup = this.nodeLookup;
|
||||
delete lookup[datum.id];
|
||||
|
||||
lookup = this.nodeTimeLookup;
|
||||
delete lookup[datum.id];
|
||||
}
|
||||
});
|
||||
|
||||
removeFromLookups(datum) {
|
||||
delete this.nodeLookup[datum.id];
|
||||
delete this.nodeTimeLookup[datum.id];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseSiteMapGenerator;
|
||||
|
|
|
@ -1,66 +1,35 @@
|
|||
var _ = require('lodash'),
|
||||
config = require('../../../config'),
|
||||
sitemap = require('./index');
|
||||
const config = require('../../../config'),
|
||||
Manager = require('./manager'),
|
||||
manager = new Manager();
|
||||
|
||||
// Responsible for handling requests for sitemap files
|
||||
module.exports = function handler(siteApp) {
|
||||
var resourceTypes = ['posts', 'authors', 'tags', 'pages'],
|
||||
verifyResourceType = function verifyResourceType(req, res, next) {
|
||||
if (!_.includes(resourceTypes, req.params.resource)) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const verifyResourceType = function verifyResourceType(req, res, next) {
|
||||
if (!manager.hasOwnProperty(req.params.resource)) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
getResourceSiteMapXml = function getResourceSiteMapXml(type, page) {
|
||||
return sitemap.getSiteMapXml(type, page);
|
||||
};
|
||||
|
||||
siteApp.get('/sitemap.xml', function sitemapXML(req, res, next) {
|
||||
var siteMapXml = sitemap.getIndexXml();
|
||||
next();
|
||||
};
|
||||
|
||||
siteApp.get('/sitemap.xml', function sitemapXML(req, res) {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=' + config.get('caching:sitemap:maxAge'),
|
||||
'Content-Type': 'text/xml'
|
||||
});
|
||||
|
||||
// CASE: returns null if sitemap is not initialized as below
|
||||
if (!siteMapXml) {
|
||||
sitemap.init()
|
||||
.then(function () {
|
||||
siteMapXml = sitemap.getIndexXml();
|
||||
res.send(siteMapXml);
|
||||
})
|
||||
.catch(function (err) {
|
||||
next(err);
|
||||
});
|
||||
} else {
|
||||
res.send(siteMapXml);
|
||||
}
|
||||
res.send(manager.getIndexXml());
|
||||
});
|
||||
|
||||
siteApp.get('/sitemap-:resource.xml', verifyResourceType, function sitemapResourceXML(req, res, next) {
|
||||
siteApp.get('/sitemap-:resource.xml', verifyResourceType, function sitemapResourceXML(req, res) {
|
||||
var type = req.params.resource,
|
||||
page = 1,
|
||||
siteMapXml = getResourceSiteMapXml(type, page);
|
||||
page = 1;
|
||||
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=' + config.get('caching:sitemap:maxAge'),
|
||||
'Content-Type': 'text/xml'
|
||||
});
|
||||
|
||||
// CASE: returns null if sitemap is not initialized
|
||||
if (!siteMapXml) {
|
||||
sitemap.init()
|
||||
.then(function () {
|
||||
siteMapXml = getResourceSiteMapXml(type, page);
|
||||
res.send(siteMapXml);
|
||||
})
|
||||
.catch(function (err) {
|
||||
next(err);
|
||||
});
|
||||
} else {
|
||||
res.send(siteMapXml);
|
||||
}
|
||||
res.send(manager.getSiteMapXml(type, page));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,27 +1,23 @@
|
|||
var _ = require('lodash'),
|
||||
xml = require('xml'),
|
||||
moment = require('moment'),
|
||||
const _ = require('lodash'),
|
||||
xml = require('xml'),
|
||||
moment = require('moment'),
|
||||
urlService = require('../../../services/url'),
|
||||
localUtils = require('./utils'),
|
||||
RESOURCES,
|
||||
XMLNS_DECLS;
|
||||
localUtils = require('./utils');
|
||||
|
||||
RESOURCES = ['pages', 'posts', 'authors', 'tags'];
|
||||
|
||||
XMLNS_DECLS = {
|
||||
const XMLNS_DECLS = {
|
||||
_attr: {
|
||||
xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9'
|
||||
}
|
||||
};
|
||||
|
||||
function SiteMapIndexGenerator(opts) {
|
||||
// Grab the other site map generators from the options
|
||||
_.extend(this, _.pick(opts, RESOURCES));
|
||||
}
|
||||
class SiteMapIndexGenerator {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
this.types = options.types;
|
||||
}
|
||||
|
||||
_.extend(SiteMapIndexGenerator.prototype, {
|
||||
getIndexXml: function () {
|
||||
var urlElements = this.generateSiteMapUrlElements(),
|
||||
getXml() {
|
||||
const urlElements = this.generateSiteMapUrlElements(),
|
||||
data = {
|
||||
// Concat the elements to the _attr declaration
|
||||
sitemapindex: [XMLNS_DECLS].concat(urlElements)
|
||||
|
@ -29,16 +25,12 @@ _.extend(SiteMapIndexGenerator.prototype, {
|
|||
|
||||
// Return the xml
|
||||
return localUtils.getDeclarations() + xml(data);
|
||||
},
|
||||
}
|
||||
|
||||
generateSiteMapUrlElements: function () {
|
||||
var self = this;
|
||||
|
||||
return _.map(RESOURCES, function (resourceType) {
|
||||
var url = urlService.utils.urlFor({
|
||||
relativeUrl: '/sitemap-' + resourceType + '.xml'
|
||||
}, true),
|
||||
lastModified = self[resourceType].lastModified;
|
||||
generateSiteMapUrlElements() {
|
||||
return _.map(this.types, (resourceType) => {
|
||||
var url = urlService.utils.urlFor({relativeUrl: '/sitemap-' + resourceType.name + '.xml'}, true),
|
||||
lastModified = resourceType.lastModified;
|
||||
|
||||
return {
|
||||
sitemap: [
|
||||
|
@ -48,6 +40,6 @@ _.extend(SiteMapIndexGenerator.prototype, {
|
|||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SiteMapIndexGenerator;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
var SiteMapManager = require('./manager');
|
||||
|
||||
module.exports = new SiteMapManager();
|
|
@ -1,75 +1,73 @@
|
|||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
const common = require('../../../lib/common'),
|
||||
IndexMapGenerator = require('./index-generator'),
|
||||
PagesMapGenerator = require('./page-generator'),
|
||||
PostsMapGenerator = require('./post-generator'),
|
||||
UsersMapGenerator = require('./user-generator'),
|
||||
TagsMapGenerator = require('./tag-generator'),
|
||||
SiteMapManager;
|
||||
TagsMapGenerator = require('./tag-generator');
|
||||
|
||||
SiteMapManager = function (opts) {
|
||||
opts = opts || {};
|
||||
class SiteMapManager {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
this.initialized = false;
|
||||
this.pages = options.pages || this.createPagesGenerator(options);
|
||||
this.posts = options.posts || this.createPostsGenerator(options);
|
||||
this.users = this.authors = options.authors || this.createUsersGenerator(options);
|
||||
this.tags = options.tags || this.createTagsGenerator(options);
|
||||
this.index = options.index || this.createIndexGenerator(options);
|
||||
|
||||
this.pages = opts.pages || this.createPagesGenerator(opts);
|
||||
this.posts = opts.posts || this.createPostsGenerator(opts);
|
||||
this.authors = opts.authors || this.createUsersGenerator(opts);
|
||||
this.tags = opts.tags || this.createTagsGenerator(opts);
|
||||
common.events.on('router.created', (router) => {
|
||||
if (router.name === 'StaticRoutesRouter') {
|
||||
this.pages.addUrl(router.getRoute({absolute: true}), {id: router.identifier, staticRoute: true});
|
||||
}
|
||||
|
||||
this.index = opts.index || this.createIndexGenerator(opts);
|
||||
};
|
||||
|
||||
_.extend(SiteMapManager.prototype, {
|
||||
createIndexGenerator: function () {
|
||||
return new IndexMapGenerator(_.pick(this, 'pages', 'posts', 'authors', 'tags'));
|
||||
},
|
||||
|
||||
createPagesGenerator: function (opts) {
|
||||
return new PagesMapGenerator(opts);
|
||||
},
|
||||
|
||||
createPostsGenerator: function (opts) {
|
||||
return new PostsMapGenerator(opts);
|
||||
},
|
||||
|
||||
createUsersGenerator: function (opts) {
|
||||
return new UsersMapGenerator(opts);
|
||||
},
|
||||
|
||||
createTagsGenerator: function (opts) {
|
||||
return new TagsMapGenerator(opts);
|
||||
},
|
||||
|
||||
init: function () {
|
||||
var self = this,
|
||||
initOps = [
|
||||
this.pages.init(),
|
||||
this.posts.init(),
|
||||
this.authors.init(),
|
||||
this.tags.init()
|
||||
];
|
||||
|
||||
return Promise.all(initOps).then(function () {
|
||||
self.initialized = true;
|
||||
if (router.name === 'CollectionRouter') {
|
||||
this.pages.addUrl(router.getRoute({absolute: true}), {id: router.identifier, staticRoute: false});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getIndexXml: function () {
|
||||
if (!this.initialized) {
|
||||
return '';
|
||||
}
|
||||
common.events.on('url.added', (obj) => {
|
||||
this[obj.resource.config.type].addUrl(obj.url.absolute, obj.resource.data);
|
||||
});
|
||||
|
||||
return this.index.getIndexXml();
|
||||
},
|
||||
|
||||
getSiteMapXml: function (type) {
|
||||
if (!this.initialized || !this[type]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this[type].siteMapContent;
|
||||
common.events.on('url.removed', (obj) => {
|
||||
this[obj.resource.config.type].removeUrl(obj.url.absolute, obj.resource.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
createIndexGenerator() {
|
||||
return new IndexMapGenerator({
|
||||
types: {
|
||||
pages: this.pages,
|
||||
posts: this.posts,
|
||||
authors: this.authors,
|
||||
tags: this.tags
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPagesGenerator(options) {
|
||||
return new PagesMapGenerator(options);
|
||||
}
|
||||
|
||||
createPostsGenerator(options) {
|
||||
return new PostsMapGenerator(options);
|
||||
}
|
||||
|
||||
createUsersGenerator(options) {
|
||||
return new UsersMapGenerator(options);
|
||||
}
|
||||
|
||||
createTagsGenerator(options) {
|
||||
return new TagsMapGenerator(options);
|
||||
}
|
||||
|
||||
getIndexXml() {
|
||||
return this.index.getXml();
|
||||
}
|
||||
|
||||
getSiteMapXml(type) {
|
||||
return this[type].getXml();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SiteMapManager;
|
||||
|
|
|
@ -1,61 +1,22 @@
|
|||
var _ = require('lodash'),
|
||||
api = require('../../../api'),
|
||||
urlService = require('../../../services/url'),
|
||||
const _ = require('lodash'),
|
||||
BaseMapGenerator = require('./base-generator');
|
||||
|
||||
// A class responsible for generating a sitemap from posts and keeping it updated
|
||||
function PageMapGenerator(opts) {
|
||||
_.extend(this, opts);
|
||||
class PageMapGenerator extends BaseMapGenerator {
|
||||
constructor(opts) {
|
||||
super();
|
||||
|
||||
BaseMapGenerator.apply(this, arguments);
|
||||
this.name = 'pages';
|
||||
|
||||
_.extend(this, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO:
|
||||
* We could influence this with priority or meta information
|
||||
*/
|
||||
getPriorityForDatum(page) {
|
||||
return page && page.staticRoute ? 1.0 : 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit from the base generator class
|
||||
_.extend(PageMapGenerator.prototype, BaseMapGenerator.prototype);
|
||||
|
||||
_.extend(PageMapGenerator.prototype, {
|
||||
bindEvents: function () {
|
||||
var self = this;
|
||||
this.dataEvents.on('page.published', self.addOrUpdateUrl.bind(self));
|
||||
this.dataEvents.on('page.published.edited', self.addOrUpdateUrl.bind(self));
|
||||
// Note: This is called if a published post is deleted
|
||||
this.dataEvents.on('page.unpublished', self.removeUrl.bind(self));
|
||||
},
|
||||
|
||||
getData: function () {
|
||||
return api.posts.browse({
|
||||
context: {
|
||||
internal: true
|
||||
},
|
||||
filter: 'visibility:public',
|
||||
status: 'published',
|
||||
staticPages: true,
|
||||
limit: 'all'
|
||||
}).then(function (resp) {
|
||||
var homePage = {
|
||||
id: 0,
|
||||
name: 'home'
|
||||
};
|
||||
return [homePage].concat(resp.posts);
|
||||
});
|
||||
},
|
||||
|
||||
validateDatum: function (datum) {
|
||||
return datum.name === 'home' || (datum.page === true && datum.visibility === 'public');
|
||||
},
|
||||
|
||||
getUrlForDatum: function (post) {
|
||||
if (post.id === 0 && !_.isEmpty(post.name)) {
|
||||
return urlService.utils.urlFor(post.name, true);
|
||||
}
|
||||
|
||||
return urlService.utils.urlFor('post', {post: post}, true);
|
||||
},
|
||||
|
||||
getPriorityForDatum: function (post) {
|
||||
// TODO: We could influence this with priority or meta information
|
||||
return post && post.name === 'home' ? 1.0 : 0.8;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PageMapGenerator;
|
||||
|
|
|
@ -1,58 +1,19 @@
|
|||
var _ = require('lodash'),
|
||||
api = require('../../../api'),
|
||||
urlService = require('../../../services/url'),
|
||||
const _ = require('lodash'),
|
||||
BaseMapGenerator = require('./base-generator');
|
||||
|
||||
// A class responsible for generating a sitemap from posts and keeping it updated
|
||||
function PostMapGenerator(opts) {
|
||||
_.extend(this, opts);
|
||||
class PostMapGenerator extends BaseMapGenerator {
|
||||
constructor(opts) {
|
||||
super();
|
||||
|
||||
BaseMapGenerator.apply(this, arguments);
|
||||
}
|
||||
this.name = 'posts';
|
||||
|
||||
// Inherit from the base generator class
|
||||
_.extend(PostMapGenerator.prototype, BaseMapGenerator.prototype);
|
||||
_.extend(this, opts);
|
||||
}
|
||||
|
||||
_.extend(PostMapGenerator.prototype, {
|
||||
bindEvents: function () {
|
||||
var self = this;
|
||||
this.dataEvents.on('post.published', self.addOrUpdateUrl.bind(self));
|
||||
this.dataEvents.on('post.published.edited', self.addOrUpdateUrl.bind(self));
|
||||
// Note: This is called if a published post is deleted
|
||||
this.dataEvents.on('post.unpublished', self.removeUrl.bind(self));
|
||||
},
|
||||
|
||||
getData: function () {
|
||||
return api.posts.browse({
|
||||
context: {
|
||||
internal: true
|
||||
},
|
||||
filter: 'visibility:public',
|
||||
status: 'published',
|
||||
staticPages: false,
|
||||
limit: 'all',
|
||||
/**
|
||||
* Is required for permalinks e.g. `/:author/:slug/`.
|
||||
* @deprecated: `author`, will be removed in Ghost 2.0
|
||||
*/
|
||||
include: 'author,tags'
|
||||
}).then(function (resp) {
|
||||
return resp.posts;
|
||||
});
|
||||
},
|
||||
|
||||
validateDatum: function (datum) {
|
||||
return datum.page === false && datum.visibility === 'public';
|
||||
},
|
||||
|
||||
getUrlForDatum: function (post) {
|
||||
return urlService.utils.urlFor('post', {post: post}, true);
|
||||
},
|
||||
|
||||
getPriorityForDatum: function (post) {
|
||||
getPriorityForDatum(post) {
|
||||
// give a slightly higher priority to featured posts
|
||||
return post.featured ? 0.9 : 0.8;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = PostMapGenerator;
|
||||
|
|
|
@ -1,50 +1,21 @@
|
|||
var _ = require('lodash'),
|
||||
api = require('../../../api'),
|
||||
urlService = require('../../../services/url'),
|
||||
const _ = require('lodash'),
|
||||
BaseMapGenerator = require('./base-generator');
|
||||
|
||||
// A class responsible for generating a sitemap from posts and keeping it updated
|
||||
function TagsMapGenerator(opts) {
|
||||
_.extend(this, opts);
|
||||
class TagsMapGenerator extends BaseMapGenerator {
|
||||
constructor(opts) {
|
||||
super();
|
||||
|
||||
BaseMapGenerator.apply(this, arguments);
|
||||
}
|
||||
this.name = 'tags';
|
||||
_.extend(this, opts);
|
||||
}
|
||||
|
||||
// Inherit from the base generator class
|
||||
_.extend(TagsMapGenerator.prototype, BaseMapGenerator.prototype);
|
||||
|
||||
_.extend(TagsMapGenerator.prototype, {
|
||||
bindEvents: function () {
|
||||
var self = this;
|
||||
this.dataEvents.on('tag.added', self.addOrUpdateUrl.bind(self));
|
||||
this.dataEvents.on('tag.edited', self.addOrUpdateUrl.bind(self));
|
||||
this.dataEvents.on('tag.deleted', self.removeUrl.bind(self));
|
||||
},
|
||||
|
||||
getData: function () {
|
||||
return api.tags.browse({
|
||||
context: {
|
||||
internal: true
|
||||
},
|
||||
filter: 'visibility:public',
|
||||
limit: 'all'
|
||||
}).then(function (resp) {
|
||||
return resp.tags;
|
||||
});
|
||||
},
|
||||
|
||||
validateDatum: function (datum) {
|
||||
return datum.visibility === 'public';
|
||||
},
|
||||
|
||||
getUrlForDatum: function (tag) {
|
||||
return urlService.utils.urlFor('tag', {tag: tag}, true);
|
||||
},
|
||||
|
||||
getPriorityForDatum: function () {
|
||||
// TODO: We could influence this with meta information
|
||||
/**
|
||||
* @TODO:
|
||||
* We could influence this with priority or meta information
|
||||
*/
|
||||
getPriorityForDatum() {
|
||||
return 0.6;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = TagsMapGenerator;
|
||||
|
|
|
@ -1,58 +1,26 @@
|
|||
var _ = require('lodash'),
|
||||
api = require('../../../api'),
|
||||
urlService = require('../../../services/url'),
|
||||
validator = require('validator'),
|
||||
BaseMapGenerator = require('./base-generator'),
|
||||
// @TODO: figure out a way to get rid of this
|
||||
activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'];
|
||||
const _ = require('lodash'),
|
||||
validator = require('validator'),
|
||||
BaseMapGenerator = require('./base-generator');
|
||||
|
||||
// A class responsible for generating a sitemap from posts and keeping it updated
|
||||
function UserMapGenerator(opts) {
|
||||
_.extend(this, opts);
|
||||
class UserMapGenerator extends BaseMapGenerator {
|
||||
constructor(opts) {
|
||||
super();
|
||||
|
||||
BaseMapGenerator.apply(this, arguments);
|
||||
}
|
||||
this.name = 'authors';
|
||||
_.extend(this, opts);
|
||||
}
|
||||
|
||||
// Inherit from the base generator class
|
||||
_.extend(UserMapGenerator.prototype, BaseMapGenerator.prototype);
|
||||
|
||||
_.extend(UserMapGenerator.prototype, {
|
||||
bindEvents: function () {
|
||||
var self = this;
|
||||
this.dataEvents.on('user.activated', self.addOrUpdateUrl.bind(self));
|
||||
this.dataEvents.on('user.activated.edited', self.addOrUpdateUrl.bind(self));
|
||||
this.dataEvents.on('user.deactivated', self.removeUrl.bind(self));
|
||||
},
|
||||
|
||||
getData: function () {
|
||||
return api.users.browse({
|
||||
context: {
|
||||
internal: true
|
||||
},
|
||||
filter: 'visibility:public',
|
||||
status: 'active',
|
||||
limit: 'all'
|
||||
}).then(function (resp) {
|
||||
return resp.users;
|
||||
});
|
||||
},
|
||||
|
||||
validateDatum: function (datum) {
|
||||
return datum.visibility === 'public' && _.includes(activeStates, datum.status);
|
||||
},
|
||||
|
||||
getUrlForDatum: function (user) {
|
||||
return urlService.utils.urlFor('author', {author: user}, true);
|
||||
},
|
||||
|
||||
getPriorityForDatum: function () {
|
||||
// TODO: We could influence this with meta information
|
||||
/**
|
||||
* @TODO:
|
||||
* We could influence this with priority or meta information
|
||||
*/
|
||||
getPriorityForDatum() {
|
||||
return 0.6;
|
||||
},
|
||||
}
|
||||
|
||||
validateImageUrl: function (imageUrl) {
|
||||
validateImageUrl(imageUrl) {
|
||||
return imageUrl && validator.isURL(imageUrl, {protocols: ['http', 'https'], require_protocol: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = UserMapGenerator;
|
||||
|
|
|
@ -9,13 +9,12 @@
|
|||
//
|
||||
// Block helper: `{{#author}}{{/author}}`
|
||||
// This is the default handlebars behaviour of dropping into the author object scope
|
||||
|
||||
var proxy = require('./proxy'),
|
||||
const proxy = require('./proxy'),
|
||||
_ = require('lodash'),
|
||||
urlService = require('../services/url'),
|
||||
SafeString = proxy.SafeString,
|
||||
handlebars = proxy.hbs.handlebars,
|
||||
templates = proxy.templates,
|
||||
url = proxy.url;
|
||||
templates = proxy.templates;
|
||||
|
||||
/**
|
||||
* @deprecated: will be removed in Ghost 2.0
|
||||
|
@ -25,13 +24,13 @@ module.exports = function author(options) {
|
|||
return handlebars.helpers.with.call(this, this.author, options);
|
||||
}
|
||||
|
||||
var autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true,
|
||||
output = '';
|
||||
const autolink = _.isString(options.hash.autolink) && options.hash.autolink === 'false' ? false : true;
|
||||
let output = '';
|
||||
|
||||
if (this.author && this.author.name) {
|
||||
if (autolink) {
|
||||
output = templates.link({
|
||||
url: url.urlFor('author', {author: this.author}),
|
||||
url: urlService.getUrlByResourceId(this.author.id),
|
||||
text: _.escape(this.author.name)
|
||||
});
|
||||
} else {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
'use strict';
|
||||
// # Authors Helper
|
||||
// Usage: `{{authors}}`, `{{authors separator=' - '}}`
|
||||
//
|
||||
|
@ -5,32 +6,32 @@
|
|||
// By default, authors are separated by commas.
|
||||
//
|
||||
// Note that the standard {{#each authors}} implementation is unaffected by this helper.
|
||||
|
||||
var proxy = require('./proxy'),
|
||||
const proxy = require('./proxy'),
|
||||
_ = require('lodash'),
|
||||
urlService = require('../services/url'),
|
||||
SafeString = proxy.SafeString,
|
||||
templates = proxy.templates,
|
||||
url = proxy.url,
|
||||
models = proxy.models;
|
||||
|
||||
module.exports = function authors(options) {
|
||||
options = options || {};
|
||||
options.hash = options.hash || {};
|
||||
|
||||
var autolink = !(_.isString(options.hash.autolink) && options.hash.autolink === 'false'),
|
||||
const autolink = !(_.isString(options.hash.autolink) && options.hash.autolink === 'false'),
|
||||
separator = _.isString(options.hash.separator) ? options.hash.separator : ', ',
|
||||
prefix = _.isString(options.hash.prefix) ? options.hash.prefix : '',
|
||||
suffix = _.isString(options.hash.suffix) ? options.hash.suffix : '',
|
||||
limit = options.hash.limit ? parseInt(options.hash.limit, 10) : undefined,
|
||||
visibilityArr = models.Base.Model.parseVisibilityString(options.hash.visibility);
|
||||
|
||||
let output = '',
|
||||
from = options.hash.from ? parseInt(options.hash.from, 10) : 1,
|
||||
to = options.hash.to ? parseInt(options.hash.to, 10) : undefined,
|
||||
visibilityArr = models.Base.Model.parseVisibilityString(options.hash.visibility),
|
||||
output = '';
|
||||
to = options.hash.to ? parseInt(options.hash.to, 10) : undefined;
|
||||
|
||||
function createAuthorsList(authors) {
|
||||
function processAuthor(author) {
|
||||
return autolink ? templates.link({
|
||||
url: url.urlFor('author', {author: author}),
|
||||
url: urlService.getUrlByResourceId(author.id),
|
||||
text: _.escape(author.name)
|
||||
}) : _.escape(author.name);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
// `absolute` flag outputs absolute URL, else URL is relative.
|
||||
|
||||
var proxy = require('./proxy'),
|
||||
url = proxy.url;
|
||||
urlService = proxy.urlService;
|
||||
|
||||
module.exports = function imgUrl(attr, options) {
|
||||
// CASE: if no attribute is passed, e.g. `{{img_url}}` we show a warning
|
||||
|
@ -27,7 +27,7 @@ module.exports = function imgUrl(attr, options) {
|
|||
}
|
||||
|
||||
if (attr) {
|
||||
return url.urlFor('image', {image: attr}, absolute);
|
||||
return urlService.utils.urlFor('image', {image: attr}, absolute);
|
||||
}
|
||||
|
||||
// CASE: if you pass e.g. cover_image, but it is not set, then attr is null!
|
||||
|
|
|
@ -63,6 +63,6 @@ module.exports = {
|
|||
// Various utils, needs cleaning up / simplifying
|
||||
socialUrls: require('../lib/social/urls'),
|
||||
blogIcon: require('../lib/image/blog-icon'),
|
||||
url: require('../services/url').utils,
|
||||
urlService: require('../services/url'),
|
||||
localUtils: require('./utils')
|
||||
};
|
||||
|
|
|
@ -5,32 +5,32 @@
|
|||
// By default, tags are separated by commas.
|
||||
//
|
||||
// Note that the standard {{#each tags}} implementation is unaffected by this helper
|
||||
|
||||
var proxy = require('./proxy'),
|
||||
const proxy = require('./proxy'),
|
||||
_ = require('lodash'),
|
||||
urlService = proxy.urlService,
|
||||
SafeString = proxy.SafeString,
|
||||
templates = proxy.templates,
|
||||
url = proxy.url,
|
||||
models = proxy.models;
|
||||
|
||||
module.exports = function tags(options) {
|
||||
options = options || {};
|
||||
options.hash = options.hash || {};
|
||||
|
||||
var autolink = !(_.isString(options.hash.autolink) && options.hash.autolink === 'false'),
|
||||
const autolink = !(_.isString(options.hash.autolink) && options.hash.autolink === 'false'),
|
||||
separator = _.isString(options.hash.separator) ? options.hash.separator : ', ',
|
||||
prefix = _.isString(options.hash.prefix) ? options.hash.prefix : '',
|
||||
suffix = _.isString(options.hash.suffix) ? options.hash.suffix : '',
|
||||
limit = options.hash.limit ? parseInt(options.hash.limit, 10) : undefined,
|
||||
visibilityArr = models.Base.Model.parseVisibilityString(options.hash.visibility);
|
||||
|
||||
let output = '',
|
||||
from = options.hash.from ? parseInt(options.hash.from, 10) : 1,
|
||||
to = options.hash.to ? parseInt(options.hash.to, 10) : undefined,
|
||||
visibilityArr = models.Base.Model.parseVisibilityString(options.hash.visibility),
|
||||
output = '';
|
||||
to = options.hash.to ? parseInt(options.hash.to, 10) : undefined;
|
||||
|
||||
function createTagList(tags) {
|
||||
function processTag(tag) {
|
||||
return autolink ? templates.link({
|
||||
url: url.urlFor('tag', {tag: tag}),
|
||||
url: urlService.getUrlByResourceId(tag.id),
|
||||
text: _.escape(tag.name)
|
||||
}) : _.escape(tag.name);
|
||||
}
|
||||
|
|
|
@ -68,8 +68,6 @@ function init() {
|
|||
debug('Permissions done');
|
||||
return Promise.join(
|
||||
themes.init(),
|
||||
// Initialize apps
|
||||
apps.init(),
|
||||
// Initialize xmrpc ping
|
||||
xmlrpc.listen(),
|
||||
// Initialize slack ping
|
||||
|
@ -89,6 +87,17 @@ function init() {
|
|||
}
|
||||
|
||||
debug('Express Apps done');
|
||||
}).then(function () {
|
||||
/**
|
||||
* @NOTE:
|
||||
*
|
||||
* Must happen after express app bootstrapping, because we need to ensure that all
|
||||
* routers are created and are now ready to register additional routes. In this specific case, we
|
||||
* are waiting that the AppRouter was instantiated. And then we can register e.g. amp if enabled.
|
||||
*
|
||||
* If you create a published post, the url is always stronger than any app url, which is equal.
|
||||
*/
|
||||
return apps.init();
|
||||
}).then(function () {
|
||||
parentApp.use(auth.init());
|
||||
debug('Auth done');
|
||||
|
|
|
@ -122,7 +122,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
|||
if (!model.ghostEvents) {
|
||||
model.ghostEvents = [];
|
||||
|
||||
if (options.importing) {
|
||||
// CASE: when importing or deleting content, lot's of model queries are happening in one transaction
|
||||
// lot's of model events will be triggered. we ensure we set the max listeners to infinity.
|
||||
// we are using `once` - we auto remove the listener afterwards
|
||||
if (options.importing || options.destroyAll) {
|
||||
options.transacting.setMaxListeners(0);
|
||||
}
|
||||
|
||||
|
@ -1045,7 +1048,7 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
|||
relation: 'posts_tags',
|
||||
condition: ['posts_tags.tag_id', '=', 'tags.id']
|
||||
},
|
||||
select: ['posts_tags.post_id as post_id'],
|
||||
select: ['posts_tags.post_id as post_id', 'tags.visibility'],
|
||||
whereIn: 'posts_tags.post_id',
|
||||
whereInKey: 'post_id',
|
||||
orderBy: 'sort_order'
|
||||
|
|
|
@ -396,30 +396,23 @@ Post = ghostBookshelf.Model.extend({
|
|||
* But the model layer is complex and needs specific fields in specific situations.
|
||||
*
|
||||
* ### url generation
|
||||
* - it needs the following attrs for permalinks
|
||||
* - @TODO: with dynamic routing, we no longer need default columns to fetch
|
||||
* - because with static routing Ghost generated the url on runtime and needed the following attributes:
|
||||
* - `slug`: /:slug/
|
||||
* - `published_at`: /:year/:slug
|
||||
* - `author_id`: /:author/:slug, /:primary_author/:slug
|
||||
* - @TODO: with channels, we no longer need these
|
||||
* - because the url service pre-generates urls based on the resources
|
||||
* - now, the UrlService pre-generates urls based on the resources
|
||||
* - you can ask `urlService.getUrlByResourceId(post.id)`
|
||||
* - @TODO: there is currently a bug in here
|
||||
* - you request `fields=title,url`
|
||||
* - you don't use `include=tags`
|
||||
* - your permalink is `/:primary_tag/:slug/`
|
||||
* - we won't fetch the primary tag, ends in `url = /all/my-slug/`
|
||||
* - will be auto fixed when merging channels
|
||||
* - url generator when using `findAll` or `findOne` doesn't work either when using e.g. `columns: title`
|
||||
* - this is because both functions don't make use of `defaultColumnsToFetch`
|
||||
* - will be auto fixed when merging channels
|
||||
*
|
||||
* ### events
|
||||
* - you call `findAll` with `columns: id`
|
||||
* - then you trigger `post.save()`
|
||||
* - then you trigger `post.save()` on the response
|
||||
* - bookshelf events (`onSaving`) and model events (`emitChange`) are triggered
|
||||
* - @TODO: we need to disallow this
|
||||
* - but you only fetched the id column, this will trouble (!), because the event hooks require more
|
||||
* data than just the id
|
||||
* - @TODO: we need to disallow this (!)
|
||||
* - you should use `models.Post.edit(..)`
|
||||
* - editing resources denies `columns`
|
||||
* - this disallows using the `columns` option
|
||||
* - same for destroy - you should use `models.Post.destroy(...)`
|
||||
*
|
||||
* @IMPORTANT: This fn should **never** be used when updating models (models.Post.edit)!
|
||||
|
@ -466,7 +459,7 @@ Post = ghostBookshelf.Model.extend({
|
|||
}
|
||||
|
||||
if (!options.columns || (options.columns && options.columns.indexOf('url') > -1)) {
|
||||
attrs.url = urlService.utils.urlPathForPost(attrs);
|
||||
attrs.url = urlService.getUrlByResourceId(attrs.id);
|
||||
}
|
||||
|
||||
if (oldPostId) {
|
||||
|
@ -577,7 +570,8 @@ Post = ghostBookshelf.Model.extend({
|
|||
validOptions = {
|
||||
findOne: ['columns', 'importing', 'withRelated', 'require'],
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order', 'status', 'staticPages'],
|
||||
findAll: ['columns', 'filter']
|
||||
findAll: ['columns', 'filter'],
|
||||
destroy: ['destroyAll']
|
||||
};
|
||||
|
||||
// The post model additionally supports having a formats option
|
||||
|
|
|
@ -88,7 +88,8 @@ Tag = ghostBookshelf.Model.extend({
|
|||
validOptions = {
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order'],
|
||||
findAll: ['columns'],
|
||||
findOne: ['visibility']
|
||||
findOne: ['visibility'],
|
||||
destroy: ['destroyAll']
|
||||
};
|
||||
|
||||
if (validOptions[methodName]) {
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
var _ = require('lodash'),
|
||||
const _ = require('lodash'),
|
||||
api = require('../../api'),
|
||||
helpers = require('../../helpers/register'),
|
||||
filters = require('../../filters'),
|
||||
common = require('../../lib/common'),
|
||||
router = require('../route').appRouter,
|
||||
generateProxyFunctions;
|
||||
routingService = require('../routing');
|
||||
|
||||
let generateProxyFunctions;
|
||||
|
||||
generateProxyFunctions = function (name, permissions, isInternal) {
|
||||
const appRouter = routingService.registry.getRouter('appRouter');
|
||||
|
||||
var getPermission = function (perm) {
|
||||
return permissions[perm];
|
||||
},
|
||||
|
@ -77,7 +80,7 @@ generateProxyFunctions = function (name, permissions, isInternal) {
|
|||
// Expose the route service...
|
||||
routeService: {
|
||||
// This allows for mounting an entirely new Router at a path...
|
||||
registerRouter: checkRegisterPermissions('routes', router.mountRouter.bind(router))
|
||||
registerRouter: checkRegisterPermissions('routes', appRouter.mountRouter.bind(appRouter))
|
||||
},
|
||||
// Mini proxy to the API - needs review
|
||||
api: {
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
var _ = require('lodash'),
|
||||
defaultPostOptions = {};
|
||||
|
||||
class Channel {
|
||||
constructor(name, options) {
|
||||
// Set the name
|
||||
this.name = name;
|
||||
|
||||
// Store the originally passed in options
|
||||
this._origOptions = _.cloneDeep(options) || {};
|
||||
|
||||
// Setup our route
|
||||
// @TODO should a channel have a route as part of the object? Or should this live elsewhere?
|
||||
this.route = this._origOptions.route ? this.translateRoute(this._origOptions.route) : '/';
|
||||
|
||||
// Define context as name, plus any additional contexts, and don't allow duplicates
|
||||
this.context = _.union([this.name], this._origOptions.context);
|
||||
|
||||
// DATA options
|
||||
// Options for fetching related posts
|
||||
this.postOptions = _.defaults({}, defaultPostOptions, this._origOptions.postOptions);
|
||||
|
||||
// RESOURCE!!!
|
||||
// @TODO figure out a better way to declare relation to resource
|
||||
if (this._origOptions.data) {
|
||||
this.data = this._origOptions.data;
|
||||
}
|
||||
|
||||
// Template options
|
||||
// @TODO fix these HORRIBLE names
|
||||
this.slugTemplate = !!this._origOptions.slugTemplate;
|
||||
if (this._origOptions.frontPageTemplate) {
|
||||
this.frontPageTemplate = this._origOptions.frontPageTemplate;
|
||||
}
|
||||
|
||||
if (this._origOptions.editRedirect) {
|
||||
this.editRedirect = this._origOptions.editRedirect;
|
||||
}
|
||||
}
|
||||
|
||||
get isPaged() {
|
||||
return _.has(this._origOptions, 'paged') ? this._origOptions.paged : true;
|
||||
}
|
||||
|
||||
get hasRSS() {
|
||||
return _.has(this._origOptions, 'rss') ? this._origOptions.rss : true;
|
||||
}
|
||||
|
||||
translateRoute(route) {
|
||||
const routeKeywords = {
|
||||
tag: 'tag',
|
||||
author: 'author'
|
||||
};
|
||||
// @TODO find this a more general / global home, as part of the Router system,
|
||||
// so that ALL routes that get registered WITH Ghost can do this
|
||||
return route.replace(/:t_([a-zA-Z]+)/, function (fullMatch, keyword) {
|
||||
return routeKeywords[keyword];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Channel;
|
|
@ -1,165 +0,0 @@
|
|||
/* eslint-disable */
|
||||
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const EventEmitter = require('events').EventEmitter;
|
||||
const common = require('../../lib/common');
|
||||
const settingsCache = require('../settings/cache');
|
||||
|
||||
/**
|
||||
* @temporary
|
||||
*
|
||||
* This is not designed yet. This is all temporary.
|
||||
*/
|
||||
class RoutingType extends EventEmitter {
|
||||
constructor(obj) {
|
||||
super();
|
||||
|
||||
this.route = _.defaults(obj.route, {value: null, extensions: {}});
|
||||
this.config = obj.config;
|
||||
}
|
||||
|
||||
getRoute() {
|
||||
return this.route;
|
||||
}
|
||||
|
||||
getPermalinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.config.type;
|
||||
}
|
||||
|
||||
getFilter() {
|
||||
return this.config.options && this.config.options.filter;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Type: ${this.getType()}, Route: ${this.getRoute().value}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Collection extends RoutingType {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
|
||||
this.permalinks = _.defaults(obj.permalinks, {value: null, extensions: {}});
|
||||
|
||||
this.permalinks.getValue = () => {
|
||||
/**
|
||||
* @deprecated Remove in Ghost 2.0
|
||||
*/
|
||||
if (this.permalinks.value.match(/settings\.permalinks/)) {
|
||||
const value = this.permalinks.value.replace(/\/{settings\.permalinks}\//, settingsCache.get('permalinks'));
|
||||
return path.join(this.route.value, value);
|
||||
}
|
||||
|
||||
return path.join(this.route.value, this.permalinks.value);
|
||||
};
|
||||
|
||||
this._listeners();
|
||||
common.events.emit('routingType.created', this);
|
||||
}
|
||||
|
||||
getPermalinks() {
|
||||
return this.permalinks;
|
||||
}
|
||||
|
||||
_listeners() {
|
||||
/**
|
||||
* @deprecated Remove in Ghost 2.0
|
||||
*/
|
||||
if (this.getPermalinks() && this.getPermalinks().value.match(/settings\.permalinks/)) {
|
||||
common.events.on('settings.permalinks.edited', () => {
|
||||
this.emit('updated');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Type: ${this.getType()}, Route: ${this.getRoute().value}, Permalinks: ${this.getPermalinks().value}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Taxonomy extends RoutingType {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
|
||||
this.permalinks = {value: '/:slug/', extensions: {}};
|
||||
|
||||
this.permalinks.getValue = () => {
|
||||
return path.join(this.route.value, this.permalinks.value);
|
||||
};
|
||||
|
||||
common.events.emit('routingType.created', this);
|
||||
}
|
||||
|
||||
getPermalinks() {
|
||||
return this.permalinks;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Type: ${this.getType()}, Route: ${this.getRoute().value}, Permalinks: ${this.getPermalinks().value}`;
|
||||
}
|
||||
}
|
||||
|
||||
class StaticPages extends RoutingType {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
|
||||
this.permalinks = {value: '/:slug/', extensions: {}};
|
||||
|
||||
this.permalinks.getValue = () => {
|
||||
return path.join(this.route.value, this.permalinks.value);
|
||||
};
|
||||
|
||||
common.events.emit('routingType.created', this);
|
||||
}
|
||||
|
||||
getPermalinks() {
|
||||
return this.permalinks;
|
||||
}
|
||||
}
|
||||
|
||||
const collection1 = new Collection({
|
||||
route: {
|
||||
value: '/'
|
||||
},
|
||||
permalinks: {
|
||||
value: '/{settings.permalinks}/'
|
||||
},
|
||||
config: {
|
||||
type: 'posts'
|
||||
}
|
||||
});
|
||||
|
||||
const taxonomy1 = new Taxonomy({
|
||||
route: {
|
||||
value: '/author/'
|
||||
},
|
||||
config: {
|
||||
type: 'users',
|
||||
options: {}
|
||||
}
|
||||
});
|
||||
|
||||
const taxonomy2 = new Taxonomy({
|
||||
route: {
|
||||
value: '/tag/'
|
||||
},
|
||||
config: {
|
||||
type: 'tags',
|
||||
options: {}
|
||||
}
|
||||
});
|
||||
|
||||
const staticPages = new StaticPages({
|
||||
route: {
|
||||
value: '/'
|
||||
},
|
||||
config: {
|
||||
type: 'pages',
|
||||
options: {}
|
||||
}
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"index": {
|
||||
"route": "/",
|
||||
"frontPageTemplate": "home"
|
||||
},
|
||||
"tag": {
|
||||
"route": "/:t_tag/:slug/",
|
||||
"postOptions": {
|
||||
"filter": "tags:'%s'+tags.visibility:public"
|
||||
},
|
||||
"data": {
|
||||
"tag": {
|
||||
"type": "read",
|
||||
"resource": "tags",
|
||||
"options": {
|
||||
"slug": "%s",
|
||||
"visibility": "public"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slugTemplate": true,
|
||||
"editRedirect": "#/settings/tags/:slug/"
|
||||
},
|
||||
"author": {
|
||||
"route": "/:t_author/:slug/",
|
||||
"postOptions": {
|
||||
"filter": "authors:'%s'"
|
||||
},
|
||||
"data": {
|
||||
"author": {
|
||||
"type": "read",
|
||||
"resource": "users",
|
||||
"options": {
|
||||
"slug": "%s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slugTemplate": true,
|
||||
"editRedirect": "#/team/:slug/"
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* # Channel Service
|
||||
*
|
||||
* The channel service is responsible for:
|
||||
* - maintaining the config of available Channels
|
||||
* - building out the logic of how an individual Channel works
|
||||
* - providing a top level router as an entry point
|
||||
*
|
||||
* Everything else exposed via the Channel object
|
||||
*/
|
||||
|
||||
// Exports the top-level router
|
||||
module.exports = require('./parent-router');
|
|
@ -1,30 +0,0 @@
|
|||
var debug = require('ghost-ignition').debug('channels:loader'),
|
||||
_ = require('lodash'),
|
||||
path = require('path'),
|
||||
Channel = require('./Channel'),
|
||||
channels = [];
|
||||
|
||||
function loadConfig() {
|
||||
var channelConfig = {};
|
||||
|
||||
// This is a very dirty temporary hack so that we can test out channels with some Beta testers
|
||||
// If you are reading this code, and considering using it, best reach out to us on Slack
|
||||
// Definitely don't be angry at us if the structure of the JSON changes or this goes away.
|
||||
try {
|
||||
channelConfig = require(path.join(process.cwd(), 'config.channels.json'));
|
||||
} catch (err) {
|
||||
channelConfig = require('./config.channels.json');
|
||||
}
|
||||
|
||||
return channelConfig;
|
||||
}
|
||||
|
||||
module.exports.list = function list() {
|
||||
debug('Load channels start');
|
||||
_.each(loadConfig(), function (channelConfig, channelName) {
|
||||
channels.push(new Channel(channelName, channelConfig));
|
||||
});
|
||||
|
||||
debug('Load channels end');
|
||||
return channels;
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
var ParentRouter = require('../route').ParentRouter,
|
||||
loader = require('./loader'),
|
||||
channelRouter = require('./router');
|
||||
|
||||
/**
|
||||
* Channels Router
|
||||
* Parent channels router will load & mount all routes when
|
||||
* .router() is called. This allows for reloading.
|
||||
*/
|
||||
module.exports.router = function channelsRouter() {
|
||||
var channelsRouter = new ParentRouter('channels');
|
||||
|
||||
loader.list().forEach(function (channel) {
|
||||
// Create a new channelRouter, and mount it onto the parent at the correct route
|
||||
channelsRouter.mountRouter(channel.route, channelRouter(channel));
|
||||
});
|
||||
|
||||
return channelsRouter.router();
|
||||
};
|
|
@ -1,93 +0,0 @@
|
|||
var express = require('express'),
|
||||
_ = require('lodash'),
|
||||
common = require('../../lib/common'),
|
||||
urlService = require('../../services/url'),
|
||||
channelController = require('../../controllers/channel'),
|
||||
rssController = require('../../controllers/rss'),
|
||||
rssRouter,
|
||||
channelRouter;
|
||||
|
||||
function handlePageParam(req, res, next, page) {
|
||||
// routeKeywords.page: 'page'
|
||||
var pageRegex = new RegExp('/page/(.*)?/'),
|
||||
rssRegex = new RegExp('/rss/(.*)?/');
|
||||
|
||||
page = parseInt(page, 10);
|
||||
|
||||
if (page === 1) {
|
||||
// Page 1 is an alias, do a permanent 301 redirect
|
||||
if (rssRegex.test(req.url)) {
|
||||
return urlService.utils.redirect301(res, req.originalUrl.replace(rssRegex, '/rss/'));
|
||||
} else {
|
||||
return urlService.utils.redirect301(res, req.originalUrl.replace(pageRegex, '/'));
|
||||
}
|
||||
} else if (page < 1 || isNaN(page)) {
|
||||
// Nothing less than 1 is a valid page number, go straight to a 404
|
||||
return next(new common.errors.NotFoundError({message: common.i18n.t('errors.errors.pageNotFound')}));
|
||||
} else {
|
||||
// Set req.params.page to the already parsed number, and continue
|
||||
req.params.page = page;
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
function rssConfigMiddleware(req, res, next) {
|
||||
res.locals.channel.isRSS = true;
|
||||
next();
|
||||
}
|
||||
|
||||
function channelConfigMiddleware(channel) {
|
||||
return function doChannelConfig(req, res, next) {
|
||||
res.locals.channel = _.cloneDeep(channel);
|
||||
// @TODO refactor into to something explicit
|
||||
res._route = {type: 'channel'};
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
rssRouter = function rssRouter(channelMiddleware) {
|
||||
// @TODO move this to an RSS module
|
||||
var router = express.Router({mergeParams: true}),
|
||||
baseRoute = '/rss/',
|
||||
pageRoute = urlService.utils.urlJoin(baseRoute, ':page(\\d+)/');
|
||||
|
||||
// @TODO figure out how to collapse this into a single rule
|
||||
router.get(baseRoute, channelMiddleware, rssConfigMiddleware, rssController);
|
||||
router.get(pageRoute, channelMiddleware, rssConfigMiddleware, rssController);
|
||||
// Extra redirect rule
|
||||
router.get('/feed/', function redirectToRSS(req, res) {
|
||||
return urlService.utils.redirect301(res, urlService.utils.urlJoin(urlService.utils.getSubdir(), req.baseUrl, baseRoute));
|
||||
});
|
||||
|
||||
router.param('page', handlePageParam);
|
||||
return router;
|
||||
};
|
||||
|
||||
channelRouter = function channelRouter(channel) {
|
||||
var channelRouter = express.Router({mergeParams: true}),
|
||||
baseRoute = '/',
|
||||
// routeKeywords.page: 'page'
|
||||
pageRoute = urlService.utils.urlJoin('/page', ':page(\\d+)/'),
|
||||
middleware = [channelConfigMiddleware(channel)];
|
||||
|
||||
channelRouter.get(baseRoute, middleware, channelController);
|
||||
|
||||
if (channel.isPaged) {
|
||||
channelRouter.param('page', handlePageParam);
|
||||
channelRouter.get(pageRoute, middleware, channelController);
|
||||
}
|
||||
|
||||
if (channel.hasRSS) {
|
||||
channelRouter.use(rssRouter(middleware));
|
||||
}
|
||||
|
||||
if (channel.editRedirect) {
|
||||
channelRouter.get('/edit/', function redirect(req, res) {
|
||||
urlService.utils.redirectToAdmin(302, res, channel.editRedirect.replace(':slug', req.params.slug));
|
||||
});
|
||||
}
|
||||
|
||||
return channelRouter;
|
||||
};
|
||||
|
||||
module.exports = channelRouter;
|
|
@ -1,4 +0,0 @@
|
|||
var ParentRouter = require('./ParentRouter'),
|
||||
appRouter = new ParentRouter('apps');
|
||||
|
||||
module.exports = appRouter;
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* # Route Service
|
||||
*
|
||||
* Note: routes are patterns, not individual URLs, which have either
|
||||
* subrouters, or controllers mounted on them. There are not that many routes.
|
||||
*
|
||||
* The route service is intended to:
|
||||
* - keep track of the registered routes, and what they have mounted on them
|
||||
* - provide a way for apps to register routes
|
||||
* - expose base classes & registry to the rest of Ghost
|
||||
*/
|
||||
|
||||
// This is the main router, that gets extended & mounted /site
|
||||
module.exports.siteRouter = require('./site-router');
|
||||
|
||||
// We expose this via the App Proxy, so that Apps can register routes
|
||||
module.exports.appRouter = require('./app-router');
|
||||
|
||||
// Classes for other parts of Ghost to extend
|
||||
module.exports.ParentRouter = require('./ParentRouter');
|
||||
|
||||
module.exports.registry = require('./registry');
|
|
@ -1,12 +0,0 @@
|
|||
var _ = require('lodash'),
|
||||
routes = [];
|
||||
|
||||
module.exports = {
|
||||
set(routerName, route) {
|
||||
routes.push({route: route, from: routerName});
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return _.cloneDeep(routes);
|
||||
}
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
// Site Router is the top level Router for the whole site
|
||||
var ParentRouter = require('./ParentRouter'),
|
||||
siteRouter = new ParentRouter('site');
|
||||
|
||||
module.exports = siteRouter;
|
157
core/server/services/routing/CollectionRouter.js
Normal file
157
core/server/services/routing/CollectionRouter.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:collection-router');
|
||||
const common = require('../../lib/common');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const urlService = require('../url');
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
|
||||
const controllers = require('./controllers');
|
||||
const middlewares = require('./middlewares');
|
||||
const RSSRouter = require('./RSSRouter');
|
||||
|
||||
class CollectionRouter extends ParentRouter {
|
||||
constructor(indexRoute, object) {
|
||||
super('CollectionRouter');
|
||||
|
||||
// NOTE: index/parent route e.g. /, /podcast/, /magic/ ;)
|
||||
this.route = {
|
||||
value: indexRoute
|
||||
};
|
||||
|
||||
this.permalinks = {
|
||||
originalValue: object.permalink,
|
||||
value: object.permalink
|
||||
};
|
||||
|
||||
this.templates = object.template || [];
|
||||
|
||||
this.filter = object.filter || 'page:false';
|
||||
|
||||
/**
|
||||
* @deprecated Remove in Ghost 2.0
|
||||
*/
|
||||
if (this.permalinks.originalValue.match(/globals\.permalinks/)) {
|
||||
this.permalinks.originalValue = this.permalinks.originalValue.replace('{globals.permalinks}', '{settings.permalinks}');
|
||||
this.permalinks.value = this.permalinks.originalValue.replace('{settings.permalinks}', settingsCache.get('permalinks'));
|
||||
this.permalinks.value = urlService.utils.deduplicateDoubleSlashes(this.permalinks.value);
|
||||
}
|
||||
|
||||
this.permalinks.getValue = (options) => {
|
||||
options = options || {};
|
||||
|
||||
// @NOTE: url options are only required when registering urls in express.
|
||||
// e.g. the UrlService will access the routes and doesn't want to know about possible url options
|
||||
if (options.withUrlOptions) {
|
||||
return urlService.utils.urlJoin(this.permalinks.value, '/:options(edit)?/');
|
||||
}
|
||||
|
||||
return this.permalinks.value;
|
||||
};
|
||||
|
||||
debug(this.route, this.permalinks);
|
||||
|
||||
this._registerRoutes();
|
||||
this._listeners();
|
||||
}
|
||||
|
||||
_registerRoutes() {
|
||||
// REGISTER: context middleware for this collection
|
||||
this.router().use(this._prepareIndexContext.bind(this));
|
||||
|
||||
// REGISTER: collection route e.g. /, /podcast/
|
||||
this.mountRoute(this.route.value, controllers.collection);
|
||||
|
||||
// REGISTER: enable pagination by default
|
||||
this.router().param('page', middlewares.pageParam);
|
||||
this.mountRoute(urlService.utils.urlJoin(this.route.value, 'page', ':page(\\d+)'), controllers.collection);
|
||||
|
||||
this.rssRouter = new RSSRouter();
|
||||
|
||||
// REGISTER: enable rss by default
|
||||
this.mountRouter(this.route.value, this.rssRouter.router());
|
||||
|
||||
// REGISTER: context middleware for entries
|
||||
this.router().use(this._prepareEntryContext.bind(this));
|
||||
|
||||
// REGISTER: permalinks e.g. /:slug/, /podcast/:slug
|
||||
this.mountRoute(this.permalinks.getValue({withUrlOptions: true}), controllers.entry);
|
||||
|
||||
common.events.emit('router.created', this);
|
||||
}
|
||||
|
||||
/**
|
||||
* We attach context information of the router to the request.
|
||||
* By this we can e.g. access the router options in controllers.
|
||||
*
|
||||
* @TODO: Why do we need two context objects? O_O - refactor this out
|
||||
*/
|
||||
_prepareIndexContext(req, res, next) {
|
||||
res.locals.routerOptions = {
|
||||
filter: this.filter,
|
||||
permalinks: this.permalinks.getValue({withUrlOptions: true}),
|
||||
type: this.getType(),
|
||||
context: ['home'],
|
||||
frontPageTemplate: 'home',
|
||||
templates: this.templates.reverse(),
|
||||
identifier: this.identifier
|
||||
};
|
||||
|
||||
res._route = {
|
||||
type: 'collection'
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_prepareEntryContext(req, res, next) {
|
||||
res.locals.routerOptions.context = ['post'];
|
||||
res._route.type = 'entry';
|
||||
next();
|
||||
}
|
||||
|
||||
_listeners() {
|
||||
/**
|
||||
* @deprecated Remove in Ghost 2.0
|
||||
*/
|
||||
if (this.getPermalinks() && this.getPermalinks().originalValue.match(/settings\.permalinks/)) {
|
||||
this._onPermalinksEditedListener = this._onPermalinksEdited.bind(this);
|
||||
common.events.on('settings.permalinks.edited', this._onPermalinksEditedListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We unmount and mount the permalink url. This enables the ability to change urls on runtime.
|
||||
*/
|
||||
_onPermalinksEdited() {
|
||||
this.unmountRoute(this.permalinks.getValue({withUrlOptions: true}));
|
||||
|
||||
this.permalinks.value = this.permalinks.originalValue.replace('{settings.permalinks}', settingsCache.get('permalinks'));
|
||||
this.permalinks.value = urlService.utils.deduplicateDoubleSlashes(this.permalinks.value);
|
||||
|
||||
this.mountRoute(this.permalinks.getValue({withUrlOptions: true}), controllers.entry);
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'posts';
|
||||
}
|
||||
|
||||
getRoute(options) {
|
||||
options = options || {};
|
||||
|
||||
return urlService.utils.createUrl(this.route.value, options.absolute, options.secure);
|
||||
}
|
||||
|
||||
getRssUrl(options) {
|
||||
return urlService.utils.createUrl(urlService.utils.urlJoin(this.route.value, this.rssRouter.route.value), options.absolute, options.secure);
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this._onPermalinksEditedListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
common.events.removeListener('settings.permalinks.edited', this._onPermalinksEditedListener);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CollectionRouter;
|
|
@ -6,15 +6,24 @@
|
|||
* Only allows for .use and .get at the moment - we don't have clear use-cases for anything else yet.
|
||||
*/
|
||||
|
||||
var debug = require('ghost-ignition').debug('services:routes:ParentRouter'),
|
||||
const debug = require('ghost-ignition').debug('services:routing:ParentRouter'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
express = require('express'),
|
||||
_ = require('lodash'),
|
||||
security = require('../../lib/security'),
|
||||
urlService = require('../url'),
|
||||
// This the route registry for the whole site
|
||||
registry = require('./registry');
|
||||
|
||||
/**
|
||||
* We expose a very limited amount of express.Router via specialist methods
|
||||
*/
|
||||
class ParentRouter {
|
||||
class ParentRouter extends EventEmitter {
|
||||
constructor(name) {
|
||||
super();
|
||||
|
||||
this.identifier = security.identifier.uid(10);
|
||||
|
||||
this.name = name;
|
||||
this._router = express.Router({mergeParams: true});
|
||||
}
|
||||
|
@ -25,7 +34,7 @@ class ParentRouter {
|
|||
debug(this.name + ': mountRouter: ' + router.name);
|
||||
this._router.use(router);
|
||||
} else {
|
||||
registry.set(this.name, path);
|
||||
registry.setRoute(this.name, path);
|
||||
debug(this.name + ': mountRouter: ' + router.name + ' at ' + path);
|
||||
this._router.use(path, router);
|
||||
}
|
||||
|
@ -33,15 +42,49 @@ class ParentRouter {
|
|||
|
||||
mountRoute(path, controller) {
|
||||
debug(this.name + ': mountRoute for', path, controller.name);
|
||||
registry.set(this.name, path);
|
||||
registry.setRoute(this.name, path);
|
||||
this._router.get(path, controller);
|
||||
}
|
||||
|
||||
unmountRoute(path) {
|
||||
let indexToRemove = null;
|
||||
|
||||
_.each(this._router.stack, (item, index) => {
|
||||
if (item.path === path) {
|
||||
indexToRemove = index;
|
||||
}
|
||||
});
|
||||
|
||||
if (indexToRemove !== null) {
|
||||
this._router.stack.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
|
||||
router() {
|
||||
// @TODO: should this just be the handler that is returned?
|
||||
// return this._router.handle.bind(this._router);
|
||||
return this._router;
|
||||
}
|
||||
|
||||
getPermalinks() {
|
||||
return this.permalinks;
|
||||
}
|
||||
|
||||
getFilter() {
|
||||
return this.filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return the full route including subdirectory.
|
||||
* Do not use this function to mount routes for now, because the subdirectory is already mounted.
|
||||
*/
|
||||
getRoute(options) {
|
||||
options = options || {};
|
||||
|
||||
return urlService.utils.createUrl(this.route.value, options.absolute, options.secure);
|
||||
}
|
||||
|
||||
reset() {}
|
||||
}
|
||||
|
||||
module.exports = ParentRouter;
|
29
core/server/services/routing/PreviewRouter.js
Normal file
29
core/server/services/routing/PreviewRouter.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const ParentRouter = require('./ParentRouter');
|
||||
const urlService = require('../url');
|
||||
const controllers = require('./controllers');
|
||||
|
||||
class PreviewRouter extends ParentRouter {
|
||||
constructor() {
|
||||
super('PreviewRouter');
|
||||
|
||||
this.route = {value: '/p/'};
|
||||
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
||||
_registerRoutes() {
|
||||
this.router().use(this._prepareContext.bind(this));
|
||||
|
||||
this.mountRoute(urlService.utils.urlJoin(this.route.value, ':uuid', ':options?'), controllers.preview);
|
||||
}
|
||||
|
||||
_prepareContext(req, res, next) {
|
||||
res._route = {
|
||||
type: 'entry'
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreviewRouter;
|
36
core/server/services/routing/RSSRouter.js
Normal file
36
core/server/services/routing/RSSRouter.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
const ParentRouter = require('./ParentRouter');
|
||||
const urlService = require('../url');
|
||||
|
||||
const controllers = require('./controllers');
|
||||
const middlewares = require('./middlewares');
|
||||
|
||||
class RSSRouter extends ParentRouter {
|
||||
constructor() {
|
||||
super('RSSRouter');
|
||||
|
||||
this.route = {value: '/rss/'};
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
||||
_registerRoutes() {
|
||||
this.mountRoute(this.route.value, controllers.rss);
|
||||
|
||||
// REGISTER: pagination
|
||||
this.router().param('page', middlewares.pageParam);
|
||||
this.mountRoute(urlService.utils.urlJoin(this.route.value, ':page(\\d+)'), controllers.rss);
|
||||
|
||||
// REGISTER: redirect rule
|
||||
this.mountRoute('/feed/', this._redirectFeedRequest.bind(this));
|
||||
}
|
||||
|
||||
_redirectFeedRequest(req, res) {
|
||||
urlService
|
||||
.utils
|
||||
.redirect301(
|
||||
res,
|
||||
urlService.utils.urlJoin(urlService.utils.getSubdir(), req.baseUrl, this.route.value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RSSRouter;
|
60
core/server/services/routing/StaticPagesRouter.js
Normal file
60
core/server/services/routing/StaticPagesRouter.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:static-pages-router');
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
const controllers = require('./controllers');
|
||||
const common = require('../../lib/common');
|
||||
|
||||
class StaticPagesRouter extends ParentRouter {
|
||||
constructor() {
|
||||
super('StaticPagesRouter');
|
||||
|
||||
this.permalinks = {
|
||||
value: '/:slug/'
|
||||
};
|
||||
|
||||
this.filter = 'page:true';
|
||||
|
||||
this.permalinks.getValue = () => {
|
||||
return this.permalinks.value;
|
||||
};
|
||||
|
||||
debug(this.permalinks);
|
||||
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
||||
_registerRoutes() {
|
||||
this.router().use(this._prepareContext.bind(this));
|
||||
|
||||
// REGISTER: permalink for static pages
|
||||
this.mountRoute(this.permalinks.getValue(), controllers.entry);
|
||||
|
||||
common.events.emit('router.created', this);
|
||||
}
|
||||
|
||||
_prepareContext(req, res, next) {
|
||||
res.locals.routerOptions = {
|
||||
filter: this.filter,
|
||||
permalinks: this.permalinks.getValue(),
|
||||
type: this.getType(),
|
||||
context: ['page']
|
||||
};
|
||||
|
||||
res._route = {
|
||||
type: 'entry'
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
getType() {
|
||||
return 'pages';
|
||||
}
|
||||
|
||||
getRoute() {
|
||||
return null;
|
||||
}
|
||||
|
||||
reset() {}
|
||||
}
|
||||
|
||||
module.exports = StaticPagesRouter;
|
46
core/server/services/routing/StaticRoutesRouter.js
Normal file
46
core/server/services/routing/StaticRoutesRouter.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:static-pages-router');
|
||||
const common = require('../../lib/common');
|
||||
const helpers = require('./helpers');
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
|
||||
class StaticRoutesRouter extends ParentRouter {
|
||||
constructor(key, template) {
|
||||
super('StaticRoutesRouter');
|
||||
|
||||
this.route = {value: key};
|
||||
this.template = template;
|
||||
|
||||
debug(this.route.value, this.template);
|
||||
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
||||
_registerRoutes() {
|
||||
this.router().use(this._prepareContext.bind(this));
|
||||
|
||||
this.mountRoute(this.route.value, this._renderStaticRoute.bind(this));
|
||||
|
||||
common.events.emit('router.created', this);
|
||||
}
|
||||
|
||||
_prepareContext(req, res, next) {
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: this.template,
|
||||
defaultTemplate: 'index'
|
||||
};
|
||||
|
||||
res.locals.routerOptions = {
|
||||
context: []
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_renderStaticRoute(req, res) {
|
||||
debug('StaticRoutesRouter');
|
||||
helpers.renderer(req, res, {});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StaticRoutesRouter;
|
108
core/server/services/routing/TaxonomyRouter.js
Normal file
108
core/server/services/routing/TaxonomyRouter.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:taxonomy-router');
|
||||
const common = require('../../lib/common');
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
const RSSRouter = require('./RSSRouter');
|
||||
const urlService = require('../url');
|
||||
const controllers = require('./controllers');
|
||||
const middlewares = require('./middlewares');
|
||||
|
||||
/* eslint-disable */
|
||||
const knownTaxonomies = {
|
||||
tag: {
|
||||
filter: "tags:'%s'+tags.visibility:public",
|
||||
data: {
|
||||
type: 'read',
|
||||
resource: 'tags',
|
||||
options: {
|
||||
slug: '%s',
|
||||
visibility: 'public'
|
||||
}
|
||||
},
|
||||
editRedirect: '#/settings/tags/:slug/'
|
||||
},
|
||||
author: {
|
||||
filter: "authors:'%s'",
|
||||
data: {
|
||||
type: 'read',
|
||||
resource: 'users',
|
||||
options: {
|
||||
slug: '%s',
|
||||
visibility: 'public'
|
||||
}
|
||||
},
|
||||
editRedirect: '#/team/:slug/'
|
||||
}
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
class TaxonomyRouter extends ParentRouter {
|
||||
constructor(key, permalinks) {
|
||||
super('Taxonomy');
|
||||
|
||||
this.taxonomyKey = key;
|
||||
|
||||
this.permalinks = {
|
||||
value: permalinks
|
||||
};
|
||||
|
||||
this.permalinks.getValue = () => {
|
||||
return this.permalinks.value;
|
||||
};
|
||||
|
||||
debug(this.permalinks);
|
||||
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
||||
_registerRoutes() {
|
||||
// REGISTER: context middleware
|
||||
this.router().use(this._prepareContext.bind(this));
|
||||
|
||||
// REGISTER: enable rss by default
|
||||
this.mountRouter(this.permalinks.getValue(), new RSSRouter().router());
|
||||
|
||||
// REGISTER: e.g. /tag/:slug/
|
||||
this.mountRoute(this.permalinks.getValue(), controllers.collection);
|
||||
|
||||
// REGISTER: enable pagination for each taxonomy by default
|
||||
this.router().param('page', middlewares.pageParam);
|
||||
this.mountRoute(urlService.utils.urlJoin(this.permalinks.value, 'page', ':page(\\d+)'), controllers.collection);
|
||||
|
||||
this.mountRoute(urlService.utils.urlJoin(this.permalinks.value, 'edit'), this._redirectEditOption.bind(this));
|
||||
|
||||
common.events.emit('router.created', this);
|
||||
}
|
||||
|
||||
_prepareContext(req, res, next) {
|
||||
res.locals.routerOptions = {
|
||||
name: this.taxonomyKey,
|
||||
permalinks: this.permalinks.getValue(),
|
||||
data: {[this.taxonomyKey]: knownTaxonomies[this.taxonomyKey].data},
|
||||
filter: knownTaxonomies[this.taxonomyKey].filter,
|
||||
type: this.getType(),
|
||||
context: [this.taxonomyKey],
|
||||
slugTemplate: true,
|
||||
identifier: this.identifier
|
||||
};
|
||||
|
||||
res._route = {
|
||||
type: 'collection'
|
||||
};
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
_redirectEditOption(req, res) {
|
||||
urlService.utils.redirectToAdmin(302, res, knownTaxonomies[this.taxonomyKey].editRedirect.replace(':slug', req.params.slug));
|
||||
}
|
||||
|
||||
getType() {
|
||||
return knownTaxonomies[this.taxonomyKey].data.resource;
|
||||
}
|
||||
|
||||
getRoute() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TaxonomyRouter;
|
68
core/server/services/routing/bootstrap.js
vendored
Normal file
68
core/server/services/routing/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,68 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:bootstrap');
|
||||
const _ = require('lodash');
|
||||
const settingsService = require('../settings');
|
||||
const StaticRoutesRouter = require('./StaticRoutesRouter');
|
||||
const StaticPagesRouter = require('./StaticPagesRouter');
|
||||
const CollectionRouter = require('./CollectionRouter');
|
||||
const TaxonomyRouter = require('./TaxonomyRouter');
|
||||
const PreviewRouter = require('./PreviewRouter');
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
|
||||
const registry = require('./registry');
|
||||
|
||||
/**
|
||||
* Create a set of default and dynamic routers defined in the routing yaml.
|
||||
*
|
||||
* @TODO:
|
||||
* - is the PreviewRouter an app?
|
||||
*/
|
||||
module.exports = function bootstrap() {
|
||||
debug('bootstrap');
|
||||
|
||||
registry.resetAllRouters();
|
||||
registry.resetAllRoutes();
|
||||
|
||||
const siteRouter = new ParentRouter('site');
|
||||
const previewRouter = new PreviewRouter();
|
||||
|
||||
siteRouter.mountRouter(previewRouter.router());
|
||||
|
||||
registry.setRouter('siteRouter', siteRouter);
|
||||
registry.setRouter('previewRouter', previewRouter);
|
||||
|
||||
const dynamicRoutes = settingsService.get('routes');
|
||||
|
||||
_.each(dynamicRoutes.taxonomies, (value, key) => {
|
||||
const taxonomyRouter = new TaxonomyRouter(key, value);
|
||||
siteRouter.mountRouter(taxonomyRouter.router());
|
||||
|
||||
registry.setRouter(taxonomyRouter.identifier, taxonomyRouter);
|
||||
});
|
||||
|
||||
_.each(dynamicRoutes.routes, (value, key) => {
|
||||
const staticRoutesRouter = new StaticRoutesRouter(key, value);
|
||||
siteRouter.mountRouter(staticRoutesRouter.router());
|
||||
|
||||
registry.setRouter(staticRoutesRouter.identifier, staticRoutesRouter);
|
||||
});
|
||||
|
||||
_.each(dynamicRoutes.collections, (value, key) => {
|
||||
const collectionRouter = new CollectionRouter(key, value);
|
||||
siteRouter.mountRouter(collectionRouter.router());
|
||||
|
||||
registry.setRouter(collectionRouter.identifier, collectionRouter);
|
||||
});
|
||||
|
||||
const staticPagesRouter = new StaticPagesRouter();
|
||||
siteRouter.mountRouter(staticPagesRouter.router());
|
||||
|
||||
registry.setRouter('staticPagesRouter', staticPagesRouter);
|
||||
|
||||
const appRouter = new ParentRouter('apps');
|
||||
siteRouter.mountRouter(appRouter.router());
|
||||
|
||||
registry.setRouter('appRouter', appRouter);
|
||||
|
||||
debug('Routes:', registry.getAllRoutes());
|
||||
return siteRouter.router();
|
||||
};
|
54
core/server/services/routing/controllers/collection.js
Normal file
54
core/server/services/routing/controllers/collection.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
const _ = require('lodash'),
|
||||
debug = require('ghost-ignition').debug('services:routing:controllers:collection'),
|
||||
common = require('../../../lib/common'),
|
||||
security = require('../../../lib/security'),
|
||||
themes = require('../../themes'),
|
||||
filters = require('../../../filters'),
|
||||
helpers = require('../helpers');
|
||||
|
||||
module.exports = function collectionController(req, res, next) {
|
||||
debug('collectionController', req.params, res.locals.routerOptions);
|
||||
|
||||
const pathOptions = {
|
||||
page: req.params.page !== undefined ? req.params.page : 1,
|
||||
slug: req.params.slug ? security.string.safe(req.params.slug) : undefined
|
||||
};
|
||||
|
||||
if (pathOptions.page) {
|
||||
const postsPerPage = parseInt(themes.getActive().config('posts_per_page'));
|
||||
|
||||
// CASE: no negative posts per page
|
||||
if (!isNaN(postsPerPage) && postsPerPage > 0) {
|
||||
pathOptions.limit = postsPerPage;
|
||||
}
|
||||
}
|
||||
|
||||
return helpers.fetchData(pathOptions, res.locals.routerOptions)
|
||||
.then(function handleResult(result) {
|
||||
// CASE: requested page is greater than number of pages we have
|
||||
if (pathOptions.page > result.meta.pagination.pages) {
|
||||
return next(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.errors.pageNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
// Format data 1
|
||||
// @TODO: figure out if this can be removed, it's supposed to ensure that absolutely URLs get generated
|
||||
// correctly for the various objects, but I believe it doesn't work and a different approach is needed.
|
||||
helpers.secure(req, result.posts);
|
||||
|
||||
// @TODO: get rid of this O_O
|
||||
_.each(result.data, function (data) {
|
||||
helpers.secure(req, data);
|
||||
});
|
||||
|
||||
// @TODO: properly design these filters
|
||||
filters.doFilter('prePostsRender', result.posts, res.locals)
|
||||
.then(function (posts) {
|
||||
result.posts = posts;
|
||||
return result;
|
||||
})
|
||||
.then(helpers.renderCollection(req, res));
|
||||
})
|
||||
.catch(helpers.handleError(next));
|
||||
};
|
78
core/server/services/routing/controllers/entry.js
Normal file
78
core/server/services/routing/controllers/entry.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:controllers:entry'),
|
||||
urlService = require('../../url'),
|
||||
filters = require('../../../filters'),
|
||||
helpers = require('../helpers');
|
||||
|
||||
/**
|
||||
* @TODO:
|
||||
* - use `filter` for `findOne`?
|
||||
* - always execute `next` until no router want's to serve and 404's
|
||||
*/
|
||||
module.exports = function entryController(req, res, next) {
|
||||
debug('entryController', res.locals.routerOptions);
|
||||
|
||||
return helpers.postLookup(req.path, res.locals.routerOptions)
|
||||
.then(function then(lookup) {
|
||||
// Format data 1
|
||||
const post = lookup ? lookup.post : false;
|
||||
|
||||
if (!post) {
|
||||
debug('no post');
|
||||
return next();
|
||||
}
|
||||
|
||||
// CASE: postlookup can detect options for example /edit, unknown options get ignored and end in 404
|
||||
if (lookup.isUnknownOption) {
|
||||
debug('isUnknownOption');
|
||||
return next();
|
||||
}
|
||||
|
||||
// CASE: last param is of url is /edit, redirect to admin
|
||||
if (lookup.isEditURL) {
|
||||
debug('redirect. is edit url');
|
||||
return urlService.utils.redirectToAdmin(302, res, '/editor/' + post.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* CASE: check if type of router owns this resource
|
||||
*
|
||||
* Static pages have a hardcoded permalink, which is `/:slug/`.
|
||||
* Imagine you define a collection under `/` with the permalink `/:slug/`.
|
||||
*
|
||||
* The router hierarchy is:
|
||||
*
|
||||
* 1. collections
|
||||
* 2. static pages
|
||||
*
|
||||
* Both permalinks are registered in express. If you serve a static page, the
|
||||
* collection router will try to serve this as a post resource.
|
||||
*
|
||||
* That's why we have to check against the router type.
|
||||
*/
|
||||
if (urlService.getResource(post.url).config.type !== res.locals.routerOptions.type) {
|
||||
debug('not my resource type');
|
||||
return next();
|
||||
}
|
||||
|
||||
/**
|
||||
* CASE: Permalink is not valid anymore, we redirect him permanently to the correct one
|
||||
* This should only happen if you have date permalinks enabled and you change
|
||||
* your publish date.
|
||||
*
|
||||
* @NOTE
|
||||
*
|
||||
* The resource url always contains the subdirectory. This was different before dynamic routing.
|
||||
* That's why we have to use the original url, which contains the sub-directory.
|
||||
*/
|
||||
if (post.url !== req.originalUrl) {
|
||||
debug('redirect');
|
||||
return urlService.utils.redirect301(res, post.url);
|
||||
}
|
||||
|
||||
helpers.secure(req, post);
|
||||
|
||||
filters.doFilter('prePostsRender', post, res.locals)
|
||||
.then(helpers.renderEntry(req, res));
|
||||
})
|
||||
.catch(helpers.handleError(next));
|
||||
};
|
17
core/server/services/routing/controllers/index.js
Normal file
17
core/server/services/routing/controllers/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
get entry() {
|
||||
return require('./entry');
|
||||
},
|
||||
|
||||
get collection() {
|
||||
return require('./collection');
|
||||
},
|
||||
|
||||
get rss() {
|
||||
return require('./rss');
|
||||
},
|
||||
|
||||
get preview() {
|
||||
return require('./preview');
|
||||
}
|
||||
};
|
42
core/server/services/routing/controllers/preview.js
Normal file
42
core/server/services/routing/controllers/preview.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:controllers:preview'),
|
||||
api = require('../../../api'),
|
||||
urlService = require('../../url'),
|
||||
filters = require('../../../filters'),
|
||||
helpers = require('../helpers');
|
||||
|
||||
module.exports = function previewController(req, res, next) {
|
||||
debug('previewController');
|
||||
|
||||
const params = {
|
||||
uuid: req.params.uuid,
|
||||
status: 'all',
|
||||
include: 'author,authors,tags'
|
||||
};
|
||||
|
||||
api.posts.read(params)
|
||||
.then(function then(result) {
|
||||
const post = result.posts[0];
|
||||
|
||||
if (!post) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.params.options && req.params.options.toLowerCase() === 'edit') {
|
||||
// CASE: last param of the url is /edit, redirect to admin
|
||||
return urlService.utils.redirectToAdmin(302, res, '/editor/' + post.id);
|
||||
} else if (req.params.options) {
|
||||
// CASE: unknown options param detected, ignore
|
||||
return next();
|
||||
}
|
||||
|
||||
if (post.status === 'published') {
|
||||
return urlService.utils.redirect301(res, urlService.getUrlByResourceId(post.id));
|
||||
}
|
||||
|
||||
helpers.secure(req, post);
|
||||
|
||||
filters.doFilter('prePostsRender', post, res.locals)
|
||||
.then(helpers.renderEntry(req, res));
|
||||
})
|
||||
.catch(helpers.handleError(next));
|
||||
};
|
57
core/server/services/routing/controllers/rss.js
Normal file
57
core/server/services/routing/controllers/rss.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
const _ = require('lodash'),
|
||||
debug = require('ghost-ignition').debug('services:routing:controllers:rss'),
|
||||
url = require('url'),
|
||||
common = require('../../../lib/common'),
|
||||
security = require('../../../lib/security'),
|
||||
settingsCache = require('../../settings/cache'),
|
||||
rssService = require('../../rss'),
|
||||
helpers = require('../helpers');
|
||||
|
||||
// @TODO: is this the right logic? move to UrlService utils
|
||||
function getBaseUrlForRSSReq(originalUrl, pageParam) {
|
||||
return url.parse(originalUrl).pathname.replace(new RegExp('/' + pageParam + '/$'), '/');
|
||||
}
|
||||
|
||||
// @TODO: is this really correct? Should we be using meta data title?
|
||||
function getTitle(relatedData) {
|
||||
relatedData = relatedData || {};
|
||||
var titleStart = _.get(relatedData, 'author[0].name') || _.get(relatedData, 'tag[0].name') || '';
|
||||
|
||||
titleStart += titleStart ? ' - ' : '';
|
||||
return titleStart + settingsCache.get('title');
|
||||
}
|
||||
|
||||
// @TODO: the collection controller does almost the same
|
||||
module.exports = function rssController(req, res, next) {
|
||||
debug('rssController');
|
||||
|
||||
const pathOptions = {
|
||||
page: req.params.page !== undefined ? req.params.page : 1,
|
||||
slug: req.params.slug ? security.string.safe(req.params.slug) : undefined
|
||||
};
|
||||
|
||||
// CASE: we are using an rss cache - url must be normalised (without pagination)
|
||||
// @TODO: this belongs to the rss service
|
||||
const baseUrl = getBaseUrlForRSSReq(req.originalUrl, pathOptions.page);
|
||||
|
||||
helpers.fetchData(pathOptions, res.locals.routerOptions)
|
||||
.then(function formatResult(result) {
|
||||
const response = _.pick(result, ['posts', 'meta']);
|
||||
|
||||
response.title = getTitle(result.data);
|
||||
response.description = settingsCache.get('description');
|
||||
|
||||
return response;
|
||||
})
|
||||
.then(function (data) {
|
||||
// CASE: if requested page is greater than number of pages we have
|
||||
if (pathOptions.page > data.meta.pagination.pages) {
|
||||
return next(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.errors.pageNotFound')
|
||||
}));
|
||||
}
|
||||
|
||||
return rssService.render(res, baseUrl, data);
|
||||
})
|
||||
.catch(helpers.handleError(next));
|
||||
};
|
|
@ -10,10 +10,8 @@
|
|||
* 2. req.params.page - always has the page parameter, regardless of if the URL contains a keyword (RSS pages don't)
|
||||
* 3. data - used for telling the difference between posts and pages
|
||||
*/
|
||||
|
||||
var labs = require('../../services/labs'),
|
||||
|
||||
// Context patterns, should eventually come from Channel configuration
|
||||
const labs = require('../../labs'),
|
||||
// @TODO: fix this, this is app specific and should be dynamic
|
||||
// routeKeywords.private: 'private'
|
||||
privatePattern = new RegExp('^\\/private\\/'),
|
||||
// routeKeywords.subscribe: 'subscribe'
|
||||
|
@ -49,8 +47,8 @@ function setResponseContext(req, res, data) {
|
|||
}
|
||||
|
||||
// Each page can only have at most one of these
|
||||
if (res.locals.channel) {
|
||||
res.locals.context = res.locals.context.concat(res.locals.channel.context);
|
||||
if (res.locals.routerOptions) {
|
||||
res.locals.context = res.locals.context.concat(res.locals.routerOptions.context);
|
||||
} else if (privatePattern.test(res.locals.relativeUrl)) {
|
||||
res.locals.context.push('private');
|
||||
} else if (subscribePattern.test(res.locals.relativeUrl) && labs.isSet('subscribers') === true) {
|
120
core/server/services/routing/helpers/fetch-data.js
Normal file
120
core/server/services/routing/helpers/fetch-data.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* # Fetch Data
|
||||
* Dynamically build and execute queries on the API
|
||||
*/
|
||||
const _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
urlService = require('../../url'),
|
||||
api = require('../../../api'),
|
||||
defaultPostQuery = {};
|
||||
|
||||
// The default settings for a default post query
|
||||
const queryDefaults = {
|
||||
type: 'browse',
|
||||
resource: 'posts',
|
||||
options: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Default post query needs to always include author, authors & tags
|
||||
*
|
||||
* @deprecated: `author`, will be removed in Ghost 2.0
|
||||
*/
|
||||
_.extend(defaultPostQuery, queryDefaults, {
|
||||
options: {
|
||||
include: 'author,authors,tags'
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* ## Process Query
|
||||
* Takes a 'query' object, ensures that type, resource and options are set
|
||||
* Replaces occurrences of `%s` in options with slugParam
|
||||
* Converts the query config to a promise for the result
|
||||
*
|
||||
* @param {{type: String, resource: String, options: Object}} query
|
||||
* @param {String} slugParam
|
||||
* @returns {Promise} promise for an API call
|
||||
*/
|
||||
function processQuery(query, slugParam) {
|
||||
query = _.cloneDeep(query);
|
||||
|
||||
// Ensure that all the properties are filled out
|
||||
_.defaultsDeep(query, queryDefaults);
|
||||
|
||||
// Replace any slugs, see TaxonomyRouter. We replace any '%s' by the slug
|
||||
_.each(query.options, function (option, name) {
|
||||
query.options[name] = _.isString(option) ? option.replace(/%s/g, slugParam) : option;
|
||||
});
|
||||
|
||||
// Return a promise for the api query
|
||||
return api[query.resource][query.type](query.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Fetch Data
|
||||
* Calls out to get posts per page, builds the final posts query & builds any additional queries
|
||||
* Wraps the queries using Promise.props to ensure it gets named responses
|
||||
* Does a first round of formatting on the response, and returns
|
||||
*/
|
||||
function fetchData(pathOptions, routerOptions) {
|
||||
pathOptions = pathOptions || {};
|
||||
routerOptions = routerOptions || {};
|
||||
|
||||
let postQuery = _.cloneDeep(defaultPostQuery),
|
||||
props = {};
|
||||
|
||||
if (routerOptions.filter) {
|
||||
postQuery.options.filter = routerOptions.filter;
|
||||
}
|
||||
|
||||
if (pathOptions.hasOwnProperty('page')) {
|
||||
postQuery.options.page = pathOptions.page;
|
||||
}
|
||||
|
||||
if (pathOptions.hasOwnProperty('limit')) {
|
||||
postQuery.options.limit = pathOptions.limit;
|
||||
}
|
||||
|
||||
// CASE: always fetch post collection
|
||||
// The filter can in theory contain a "%s" e.g. filter="primary_tag:%s"
|
||||
props.posts = processQuery(postQuery, pathOptions.slug);
|
||||
|
||||
// CASE: fetch more data defined by the router e.g. tags, authors - see TaxonomyRouter
|
||||
_.each(routerOptions.data, function (query, name) {
|
||||
props[name] = processQuery(query, pathOptions.slug);
|
||||
});
|
||||
|
||||
return Promise.props(props)
|
||||
.then(function formatResponse(results) {
|
||||
const response = _.cloneDeep(results.posts);
|
||||
delete results.posts;
|
||||
|
||||
// CASE: does this post belong to this collection?
|
||||
// EXCEPTION: Taxonomies always list the posts which belong to a tag/author.
|
||||
if (!routerOptions.data && routerOptions.identifier) {
|
||||
response.posts = _.filter(response.posts, (post) => {
|
||||
if (urlService.owns(routerOptions.identifier, post.url)) {
|
||||
return post;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// process any remaining data
|
||||
if (!_.isEmpty(results)) {
|
||||
response.data = {};
|
||||
|
||||
_.each(results, function (result, name) {
|
||||
if (routerOptions.data[name].type === 'browse') {
|
||||
response.data[name] = result;
|
||||
} else {
|
||||
response.data[name] = result[routerOptions.data[name].resource];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fetchData;
|
|
@ -1,4 +1,4 @@
|
|||
var _ = require('lodash');
|
||||
const _ = require('lodash');
|
||||
|
||||
/**
|
||||
* formats variables for handlebars in multi-post contexts.
|
||||
|
@ -37,6 +37,6 @@ function formatResponse(post) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
channel: formatPageResponse,
|
||||
collection: formatPageResponse,
|
||||
entry: formatResponse
|
||||
};
|
41
core/server/services/routing/helpers/index.js
Normal file
41
core/server/services/routing/helpers/index.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
module.exports = {
|
||||
get postLookup() {
|
||||
return require('./post-lookup');
|
||||
},
|
||||
|
||||
get fetchData() {
|
||||
return require('./fetch-data');
|
||||
},
|
||||
|
||||
get renderCollection() {
|
||||
return require('./render-collection');
|
||||
},
|
||||
|
||||
get formatResponse() {
|
||||
return require('./format-response');
|
||||
},
|
||||
|
||||
get renderEntry() {
|
||||
return require('./render-entry');
|
||||
},
|
||||
|
||||
get renderer() {
|
||||
return require('./renderer');
|
||||
},
|
||||
|
||||
get templates() {
|
||||
return require('./templates');
|
||||
},
|
||||
|
||||
get secure() {
|
||||
return require('./secure');
|
||||
},
|
||||
|
||||
get handleError() {
|
||||
return require('./error');
|
||||
},
|
||||
|
||||
get context() {
|
||||
return require('./context');
|
||||
}
|
||||
};
|
54
core/server/services/routing/helpers/post-lookup.js
Normal file
54
core/server/services/routing/helpers/post-lookup.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
const _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
url = require('url'),
|
||||
debug = require('ghost-ignition').debug('services:routing:helpers:post-lookup'),
|
||||
routeMatch = require('path-match')(),
|
||||
api = require('../../../api');
|
||||
|
||||
function postLookup(postUrl, routerOptions) {
|
||||
debug(postUrl);
|
||||
|
||||
const targetPath = url.parse(postUrl).path,
|
||||
permalinks = routerOptions.permalinks;
|
||||
|
||||
let isEditURL = false;
|
||||
|
||||
// CASE: e.g. /:slug/ -> { slug: 'value' }
|
||||
const matchFunc = routeMatch(permalinks);
|
||||
const params = matchFunc(targetPath);
|
||||
|
||||
debug(params);
|
||||
|
||||
// CASE 1: no matches, resolve
|
||||
// CASE 2: params can be empty e.g. permalink is /featured/:options(edit)?/ and path is /featured/
|
||||
if (params === false || !Object.keys(params).length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// CASE: redirect if url contains `/edit/` at the end
|
||||
if (params.options && params.options.toLowerCase() === 'edit') {
|
||||
isEditURL = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query database to find post.
|
||||
*
|
||||
* @deprecated: `author`, will be removed in Ghost 2.0
|
||||
*/
|
||||
return api.posts.read(_.extend(_.pick(params, 'slug', 'id'), {include: 'author,authors,tags'}))
|
||||
.then(function then(result) {
|
||||
const post = result.posts[0];
|
||||
|
||||
if (!post) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return {
|
||||
post: post,
|
||||
isEditURL: isEditURL,
|
||||
isUnknownOption: isEditURL ? false : !!params.options
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = postLookup;
|
12
core/server/services/routing/helpers/render-collection.js
Normal file
12
core/server/services/routing/helpers/render-collection.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const debug = require('ghost-ignition').debug('services:routing:helpers:render-collection'),
|
||||
formatResponse = require('./format-response'),
|
||||
renderer = require('./renderer');
|
||||
|
||||
module.exports = function renderCollection(req, res) {
|
||||
debug('renderCollection called');
|
||||
return function renderCollection(result) {
|
||||
// Format data 2
|
||||
// Render
|
||||
return renderer(req, res, formatResponse.collection(result));
|
||||
};
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
var debug = require('ghost-ignition').debug('channels:render-post'),
|
||||
const debug = require('ghost-ignition').debug('services:routing:helpers:render-post'),
|
||||
formatResponse = require('./format-response'),
|
||||
renderer = require('./renderer');
|
||||
/*
|
||||
|
@ -10,11 +10,8 @@ var debug = require('ghost-ignition').debug('channels:render-post'),
|
|||
module.exports = function renderEntry(req, res) {
|
||||
debug('renderEntry called');
|
||||
return function renderEntry(entry) {
|
||||
// Renderer begin
|
||||
// Format data 2 - 1 is in preview/entry
|
||||
var data = formatResponse.entry(entry);
|
||||
|
||||
// Render Call
|
||||
return renderer(req, res, data);
|
||||
// Render
|
||||
return renderer(req, res, formatResponse.entry(entry));
|
||||
};
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
var debug = require('ghost-ignition').debug('renderer'),
|
||||
const debug = require('ghost-ignition').debug('services:routing:helpers:renderer'),
|
||||
setContext = require('./context'),
|
||||
templates = require('./templates');
|
||||
|
||||
|
@ -12,5 +12,6 @@ module.exports = function renderer(req, res, data) {
|
|||
// Render Call
|
||||
debug('Rendering template: ' + res._template + ' for: ' + req.originalUrl);
|
||||
debug('res.locals', res.locals);
|
||||
|
||||
res.render(res._template, data);
|
||||
};
|
|
@ -2,11 +2,12 @@
|
|||
//
|
||||
// Figure out which template should be used to render a request
|
||||
// based on the templates which are allowed, and what is available in the theme
|
||||
// TODO: consider where this should live as it deals with channels, singles, and errors
|
||||
var _ = require('lodash'),
|
||||
// TODO: consider where this should live as it deals with collections, entries, and errors
|
||||
const _ = require('lodash'),
|
||||
path = require('path'),
|
||||
config = require('../../config'),
|
||||
themes = require('../../services/themes'),
|
||||
url = require('url'),
|
||||
config = require('../../../config'),
|
||||
themes = require('../../themes'),
|
||||
_private = {};
|
||||
|
||||
/**
|
||||
|
@ -20,7 +21,7 @@ var _ = require('lodash'),
|
|||
* @returns {String[]}
|
||||
*/
|
||||
_private.getErrorTemplateHierarchy = function getErrorTemplateHierarchy(statusCode) {
|
||||
var errorCode = _.toString(statusCode),
|
||||
const errorCode = _.toString(statusCode),
|
||||
templateList = ['error'];
|
||||
|
||||
// Add error class template: E.g. error-4xx.hbs or error-5xx.hbs
|
||||
|
@ -33,30 +34,38 @@ _private.getErrorTemplateHierarchy = function getErrorTemplateHierarchy(statusCo
|
|||
};
|
||||
|
||||
/**
|
||||
* ## Get Channel Template Hierarchy
|
||||
* ## Get Collection Template Hierarchy
|
||||
*
|
||||
* Fetch the ordered list of templates that can be used to render this request.
|
||||
* 'index' is the default / fallback
|
||||
* For channels with slugs: [:channelName-:slug, :channelName, index]
|
||||
* For channels without slugs: [:channelName, index]
|
||||
* Channels can also have a front page template which is used if this is the first page of the channel, e.g. 'home.hbs'
|
||||
* For collections with slugs: [:collectionName-:slug, :collectionName, index]
|
||||
* For collections without slugs: [:collectionName, index]
|
||||
* Collections can also have a front page template which is used if this is the first page of the collections, e.g. 'home.hbs'
|
||||
*
|
||||
* @param {Object} channelOpts
|
||||
* @param {Object} routerOptions
|
||||
* @returns {String[]}
|
||||
*/
|
||||
_private.getChannelTemplateHierarchy = function getChannelTemplateHierarchy(channelOpts) {
|
||||
var templateList = ['index'];
|
||||
_private.getCollectionTemplateHierarchy = function getCollectionTemplateHierarchy(routerOptions, requestOptions) {
|
||||
const templateList = ['index'];
|
||||
|
||||
if (channelOpts.name && channelOpts.name !== 'index') {
|
||||
templateList.unshift(channelOpts.name);
|
||||
// CASE: author, tag
|
||||
if (routerOptions.name && routerOptions.name !== 'index') {
|
||||
templateList.unshift(routerOptions.name);
|
||||
|
||||
if (channelOpts.slugTemplate && channelOpts.slugParam) {
|
||||
templateList.unshift(channelOpts.name + '-' + channelOpts.slugParam);
|
||||
if (routerOptions.slugTemplate && routerOptions.slugParam) {
|
||||
templateList.unshift(routerOptions.name + '-' + routerOptions.slugParam);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelOpts.frontPageTemplate && channelOpts.postOptions.page === 1) {
|
||||
templateList.unshift(channelOpts.frontPageTemplate);
|
||||
// CASE: collections can define a template list
|
||||
if (routerOptions.templates && routerOptions.templates.length) {
|
||||
routerOptions.templates.forEach((template) => {
|
||||
templateList.unshift(template);
|
||||
});
|
||||
}
|
||||
|
||||
if (routerOptions.frontPageTemplate && (requestOptions.path === '/' || requestOptions.path === '/' && requestOptions.page === 1)) {
|
||||
templateList.unshift(routerOptions.frontPageTemplate);
|
||||
}
|
||||
|
||||
return templateList;
|
||||
|
@ -74,8 +83,8 @@ _private.getChannelTemplateHierarchy = function getChannelTemplateHierarchy(chan
|
|||
* @returns {String[]}
|
||||
*/
|
||||
_private.getEntryTemplateHierarchy = function getEntryTemplateHierarchy(postObject) {
|
||||
var templateList = ['post'],
|
||||
slugTemplate = 'post-' + postObject.slug;
|
||||
const templateList = ['post'];
|
||||
let slugTemplate = 'post-' + postObject.slug;
|
||||
|
||||
if (postObject.page) {
|
||||
templateList.unshift('page');
|
||||
|
@ -101,7 +110,7 @@ _private.getEntryTemplateHierarchy = function getEntryTemplateHierarchy(postObje
|
|||
* @param {String} fallback - a fallback template
|
||||
*/
|
||||
_private.pickTemplate = function pickTemplate(templateList, fallback) {
|
||||
var template;
|
||||
let template;
|
||||
|
||||
if (!_.isArray(templateList)) {
|
||||
templateList = [templateList];
|
||||
|
@ -111,37 +120,45 @@ _private.pickTemplate = function pickTemplate(templateList, fallback) {
|
|||
template = fallback;
|
||||
} else {
|
||||
template = _.find(templateList, function (template) {
|
||||
if (!template) {
|
||||
return;
|
||||
}
|
||||
|
||||
return themes.getActive().hasTemplate(template);
|
||||
});
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
template = fallback;
|
||||
if (!fallback) {
|
||||
template = 'index';
|
||||
} else {
|
||||
template = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
_private.getTemplateForEntry = function getTemplateForEntry(postObject) {
|
||||
var templateList = _private.getEntryTemplateHierarchy(postObject),
|
||||
const templateList = _private.getEntryTemplateHierarchy(postObject),
|
||||
fallback = templateList[templateList.length - 1];
|
||||
return _private.pickTemplate(templateList, fallback);
|
||||
};
|
||||
|
||||
_private.getTemplateForChannel = function getTemplateForChannel(channelOpts) {
|
||||
var templateList = _private.getChannelTemplateHierarchy(channelOpts),
|
||||
_private.getTemplateForCollection = function getTemplateForCollection(routerOptions, requestOptions) {
|
||||
const templateList = _private.getCollectionTemplateHierarchy(routerOptions, requestOptions),
|
||||
fallback = templateList[templateList.length - 1];
|
||||
return _private.pickTemplate(templateList, fallback);
|
||||
};
|
||||
|
||||
_private.getTemplateForError = function getTemplateForError(statusCode) {
|
||||
var templateList = _private.getErrorTemplateHierarchy(statusCode),
|
||||
const templateList = _private.getErrorTemplateHierarchy(statusCode),
|
||||
fallback = path.resolve(config.get('paths').defaultViews, 'error.hbs');
|
||||
return _private.pickTemplate(templateList, fallback);
|
||||
};
|
||||
|
||||
module.exports.setTemplate = function setTemplate(req, res, data) {
|
||||
var routeConfig = res._route || {};
|
||||
const routeConfig = res._route || {};
|
||||
|
||||
if (res._template && !req.err) {
|
||||
return;
|
||||
|
@ -156,8 +173,11 @@ module.exports.setTemplate = function setTemplate(req, res, data) {
|
|||
case 'custom':
|
||||
res._template = _private.pickTemplate(routeConfig.templateName, routeConfig.defaultTemplate);
|
||||
break;
|
||||
case 'channel':
|
||||
res._template = _private.getTemplateForChannel(res.locals.channel);
|
||||
case 'collection':
|
||||
res._template = _private.getTemplateForCollection(res.locals.routerOptions, {
|
||||
path: url.parse(req.url).pathname,
|
||||
page: req.params.page
|
||||
});
|
||||
break;
|
||||
case 'entry':
|
||||
res._template = _private.getTemplateForEntry(data.post);
|
21
core/server/services/routing/index.js
Normal file
21
core/server/services/routing/index.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
get bootstrap() {
|
||||
return require('./bootstrap');
|
||||
},
|
||||
|
||||
get registry() {
|
||||
return require('./registry');
|
||||
},
|
||||
|
||||
get helpers() {
|
||||
return require('./helpers');
|
||||
},
|
||||
|
||||
get CollectionRouter() {
|
||||
return require('./CollectionRouter');
|
||||
},
|
||||
|
||||
get TaxonomyRouter() {
|
||||
return require('./TaxonomyRouter');
|
||||
}
|
||||
};
|
5
core/server/services/routing/middlewares/index.js
Normal file
5
core/server/services/routing/middlewares/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
get pageParam() {
|
||||
return require('./page-param');
|
||||
}
|
||||
};
|
27
core/server/services/routing/middlewares/page-param.js
Normal file
27
core/server/services/routing/middlewares/page-param.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const common = require('../../../lib/common/index'),
|
||||
urlService = require('../../url/index');
|
||||
|
||||
module.exports = function handlePageParam(req, res, next, page) {
|
||||
// routeKeywords.page: 'page'
|
||||
const pageRegex = new RegExp('/page/(.*)?/'),
|
||||
rssRegex = new RegExp('/rss/(.*)?/');
|
||||
|
||||
page = parseInt(page, 10);
|
||||
|
||||
if (page === 1) {
|
||||
// CASE: page 1 is an alias for the collection index, do a permanent 301 redirect
|
||||
// @TODO: this belongs into the rss router!
|
||||
if (rssRegex.test(req.url)) {
|
||||
return urlService.utils.redirect301(res, req.originalUrl.replace(rssRegex, '/rss/'));
|
||||
} else {
|
||||
return urlService.utils.redirect301(res, req.originalUrl.replace(pageRegex, '/'));
|
||||
}
|
||||
} else if (page < 1 || isNaN(page)) {
|
||||
return next(new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.errors.pageNotFound')
|
||||
}));
|
||||
} else {
|
||||
req.params.page = page;
|
||||
return next();
|
||||
}
|
||||
};
|
43
core/server/services/routing/registry.js
Normal file
43
core/server/services/routing/registry.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
const _ = require('lodash');
|
||||
let routes = [];
|
||||
let routers = {};
|
||||
|
||||
module.exports = {
|
||||
setRoute(routerName, route) {
|
||||
routes.push({route: route, from: routerName});
|
||||
},
|
||||
|
||||
setRouter(name, router) {
|
||||
routers[name] = router;
|
||||
},
|
||||
|
||||
getAllRoutes() {
|
||||
return _.cloneDeep(routes);
|
||||
},
|
||||
|
||||
getRouter(name) {
|
||||
return routers[name];
|
||||
},
|
||||
|
||||
getFirstCollectionRouter() {
|
||||
return _.find(routers, (router) => {
|
||||
if (router.name === 'CollectionRouter') {
|
||||
return router;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
resetAllRoutes() {
|
||||
routes = [];
|
||||
},
|
||||
|
||||
resetAllRouters() {
|
||||
_.each(routers, (value) => {
|
||||
value.reset();
|
||||
});
|
||||
|
||||
routers = {};
|
||||
}
|
||||
};
|
|
@ -20,7 +20,7 @@ generateTags = function generateTags(data) {
|
|||
};
|
||||
|
||||
generateItem = function generateItem(post, siteUrl, secure) {
|
||||
var itemUrl = urlService.utils.urlFor('post', {post: post, secure: secure}, true),
|
||||
var itemUrl = urlService.getUrlByResourceId(post.id, {secure: secure, absolute: true}),
|
||||
htmlContent = urlService.utils.makeAbsoluteUrls(post.html, siteUrl, itemUrl),
|
||||
item = {
|
||||
title: post.title,
|
||||
|
@ -65,7 +65,7 @@ generateItem = function generateItem(post, siteUrl, secure) {
|
|||
/**
|
||||
* Generate Feed
|
||||
*
|
||||
* Data is an object which contains the res.locals + results from fetching a channel, but without related data.
|
||||
* Data is an object which contains the res.locals + results from fetching a collection, but without related data.
|
||||
*
|
||||
* @param {string} baseUrl
|
||||
* @param {{title, description, safeVersion, secure, posts}} data
|
||||
|
|
|
@ -2,7 +2,7 @@ routes:
|
|||
|
||||
collections:
|
||||
/:
|
||||
route: '{globals.permalinks}'
|
||||
permalink: '{globals.permalinks}' # special 1.0 compatibility setting. See the docs for details.
|
||||
template:
|
||||
- home
|
||||
- index
|
||||
|
|
|
@ -6,7 +6,7 @@ const _ = require('lodash'),
|
|||
SettingsModel = require('../../models/settings').Settings,
|
||||
SettingsCache = require('./cache'),
|
||||
SettingsLoader = require('./loader'),
|
||||
// EnsureSettingsFiles = require('./ensure-settings'),
|
||||
ensureSettingsFiles = require('./ensure-settings'),
|
||||
common = require('../../lib/common'),
|
||||
debug = require('ghost-ignition').debug('services:settings:index');
|
||||
|
||||
|
@ -16,23 +16,18 @@ module.exports = {
|
|||
|
||||
debug('init settings service for:', knownSettings);
|
||||
|
||||
// TODO: uncomment this section, once we want to
|
||||
// copy the default routes.yaml file into the /content/settings
|
||||
// folder
|
||||
|
||||
// Make sure that supported settings files are available
|
||||
// inside of the `content/setting` directory
|
||||
// return EnsureSettingsFiles(knownSettings)
|
||||
// .then(() => {
|
||||
|
||||
// Update the defaults
|
||||
return SettingsModel.populateDefaults()
|
||||
.then(function (settingsCollection) {
|
||||
return ensureSettingsFiles(knownSettings)
|
||||
.then(() => {
|
||||
// Update the defaults
|
||||
return SettingsModel.populateDefaults();
|
||||
})
|
||||
.then((settingsCollection) => {
|
||||
// Initialise the cache with the result
|
||||
// This will bind to events for further updates
|
||||
SettingsCache.init(settingsCollection);
|
||||
});
|
||||
// });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,8 @@ const fs = require('fs-extra'),
|
|||
debug = require('ghost-ignition').debug('services:settings:settings-loader'),
|
||||
common = require('../../lib/common'),
|
||||
config = require('../../config'),
|
||||
yamlParser = require('./yaml-parser');
|
||||
yamlParser = require('./yaml-parser'),
|
||||
validate = require('./validate');
|
||||
|
||||
/**
|
||||
* Reads the desired settings YAML file and passes the
|
||||
|
@ -21,8 +22,8 @@ module.exports = function loadSettings(setting) {
|
|||
const file = fs.readFileSync(filePath, 'utf8');
|
||||
debug('settings file found for', setting);
|
||||
|
||||
// yamlParser returns a JSON object
|
||||
return yamlParser(file, fileName);
|
||||
const object = yamlParser(file, fileName);
|
||||
return validate(object);
|
||||
} catch (err) {
|
||||
if (common.errors.utils.isIgnitionError(err)) {
|
||||
throw err;
|
||||
|
|
205
core/server/services/settings/validate.js
Normal file
205
core/server/services/settings/validate.js
Normal file
|
@ -0,0 +1,205 @@
|
|||
const _ = require('lodash');
|
||||
const common = require('../../lib/common');
|
||||
const _private = {};
|
||||
|
||||
_private.validateRoutes = function validateRoutes(routes) {
|
||||
_.each(routes, (routingTypeObject, routingTypeObjectKey) => {
|
||||
// CASE: we hard-require trailing slashes for the index route
|
||||
if (!routingTypeObjectKey.match(/\/$/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'A trailing slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: we hard-require leading slashes for the index route
|
||||
if (!routingTypeObjectKey.match(/^\//)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'A leading slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: you define /about/:
|
||||
if (!routingTypeObject) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'Please define a permalink route.'
|
||||
}),
|
||||
help: 'e.g. permalink: /{slug}/'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
};
|
||||
|
||||
_private.validateCollections = function validateCollections(collections) {
|
||||
_.each(collections, (routingTypeObject, routingTypeObjectKey) => {
|
||||
// CASE: we hard-require trailing slashes for the collection index route
|
||||
if (!routingTypeObjectKey.match(/\/$/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'A trailing slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: we hard-require leading slashes for the collection index route
|
||||
if (!routingTypeObjectKey.match(/^\//)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'A leading slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (!routingTypeObject.hasOwnProperty('permalink')) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'Please define a permalink route.'
|
||||
}),
|
||||
help: 'e.g. permalink: /{slug}/'
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: validate permalink key
|
||||
if (routingTypeObject.hasOwnProperty('permalink')) {
|
||||
if (!routingTypeObject.permalink) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'Please define a permalink route.'
|
||||
}),
|
||||
help: 'e.g. permalink: /{slug}/'
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: we hard-require trailing slashes for the value/permalink route
|
||||
if (!routingTypeObject.permalink.match(/\/$/) && !routingTypeObject.permalink.match(/globals\.permalinks/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObject.permalink,
|
||||
reason: 'A trailing slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: we hard-require leading slashes for the value/permalink route
|
||||
if (!routingTypeObject.permalink.match(/^\//) && !routingTypeObject.permalink.match(/globals\.permalinks/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObject.permalink,
|
||||
reason: 'A leading slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: notation /:slug/ or /:primary_author/ is not allowed. We only accept /{{...}}/.
|
||||
if (routingTypeObject.permalink && routingTypeObject.permalink.match(/\/\:\w+/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObject.permalink,
|
||||
reason: 'Please use the following notation e.g. /{slug}/.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: transform {.*} into :\w+ notation. This notation is our internal notation e.g. see permalink
|
||||
// replacement in our UrlService utility.
|
||||
if (routingTypeObject.permalink.match(/{.*}/)) {
|
||||
routingTypeObject.permalink = routingTypeObject.permalink.replace(/{(\w+)}/g, ':$1');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return collections;
|
||||
};
|
||||
|
||||
_private.validateTaxonomies = function validateTaxonomies(taxonomies) {
|
||||
_.each(taxonomies, (routingTypeObject, routingTypeObjectKey) => {
|
||||
if (!routingTypeObject) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObjectKey,
|
||||
reason: 'Please define a taxonomy permalink route.'
|
||||
}),
|
||||
help: 'e.g. tag: /tag/{slug}/'
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: we hard-require trailing slashes for the taxonomie permalink route
|
||||
if (!routingTypeObject.match(/\/$/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObject,
|
||||
reason: 'A trailing slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: we hard-require leading slashes for the value/permalink route
|
||||
if (!routingTypeObject.match(/^\//)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObject,
|
||||
reason: 'A leading slash is required.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: notation /:slug/ or /:primary_author/ is not allowed. We only accept /{{...}}/.
|
||||
if (routingTypeObject && routingTypeObject.match(/\/\:\w+/)) {
|
||||
throw new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.services.settings.yaml.validate', {
|
||||
at: routingTypeObject,
|
||||
reason: 'Please use the following notation e.g. /{slug}/.'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// CASE: transform {.*} into :\w+ notation. This notation is our internal notation e.g. see permalink
|
||||
// replacement in our UrlService utility.
|
||||
if (routingTypeObject && routingTypeObject.match(/{.*}/)) {
|
||||
routingTypeObject = routingTypeObject.replace(/{(\w+)}/g, ':$1');
|
||||
taxonomies[routingTypeObjectKey] = routingTypeObject;
|
||||
}
|
||||
});
|
||||
|
||||
return taxonomies;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and sanitize the routing object.
|
||||
*/
|
||||
module.exports = function validate(object) {
|
||||
if (!object) {
|
||||
object = {};
|
||||
}
|
||||
|
||||
if (!object.routes) {
|
||||
object.routes = {};
|
||||
}
|
||||
|
||||
if (!object.collections) {
|
||||
object.collections = {};
|
||||
}
|
||||
|
||||
if (!object.taxonomies) {
|
||||
object.taxonomies = {};
|
||||
}
|
||||
|
||||
object.routes = _private.validateRoutes(object.routes);
|
||||
object.collections = _private.validateCollections(object.collections);
|
||||
object.taxonomies = _private.validateTaxonomies(object.taxonomies);
|
||||
|
||||
return object;
|
||||
};
|
|
@ -28,7 +28,7 @@ function ping(post) {
|
|||
|
||||
// If this is a post, we want to send the link of the post
|
||||
if (schema.isPost(post)) {
|
||||
message = urlService.utils.urlFor('post', {post: post}, true);
|
||||
message = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||
} else {
|
||||
message = post.message;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ const debug = require('ghost-ignition').debug('services:url:queue'),
|
|||
* Ghost fetches as earliest as possible the resources from the database. The reason is simply: we
|
||||
* want to know all urls as soon as possible.
|
||||
*
|
||||
* Parallel to this, the routes/channels are read/prepared and registered in express.
|
||||
* Parallel to this, the routes are read/prepared and registered in express.
|
||||
* So the challenge is to handle both resource availability and route registration.
|
||||
* If you start an event, all subscribers of it are executed in a sequence. The queue will wait
|
||||
* till the current subscriber has finished it's work.
|
||||
|
@ -21,9 +21,9 @@ const debug = require('ghost-ignition').debug('services:url:queue'),
|
|||
*
|
||||
* - you can re-run an event
|
||||
* - you can add more subscribers to an existing queue
|
||||
* - you can order subscribers (helpful if you want to order routes/channels)
|
||||
* - you can order subscribers (helpful if you want to order routers)
|
||||
*
|
||||
* Each subscriber represents one instance of the url generator. One url generator represents one channel/route.
|
||||
* Each subscriber represents one instance of the url generator. One url generator represents one router.
|
||||
*
|
||||
* ### Tolerance option
|
||||
*
|
||||
|
@ -201,6 +201,14 @@ class Queue extends EventEmitter {
|
|||
|
||||
this.toNotify = {};
|
||||
}
|
||||
|
||||
softReset() {
|
||||
_.each(this.toNotify, (obj) => {
|
||||
clearTimeout(obj.timeout);
|
||||
});
|
||||
|
||||
this.toNotify = {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Queue;
|
||||
|
|
|
@ -246,6 +246,14 @@ class Resources {
|
|||
this.listeners = [];
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
softReset() {
|
||||
this.data = {};
|
||||
|
||||
_.each(resourcesConfig, (resourceConfig) => {
|
||||
this.data[resourceConfig.type] = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Resources;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
const _ = require('lodash'),
|
||||
moment = require('moment-timezone'),
|
||||
jsonpath = require('jsonpath'),
|
||||
debug = require('ghost-ignition').debug('services:url:generator'),
|
||||
localUtils = require('./utils'),
|
||||
settingsCache = require('../settings/cache'),
|
||||
/**
|
||||
* @TODO: This is a fake version of the upcoming GQL tool.
|
||||
* GQL will offer a tool to match a JSON against a filter.
|
||||
|
@ -20,8 +18,8 @@ const _ = require('lodash'),
|
|||
};
|
||||
|
||||
class UrlGenerator {
|
||||
constructor(routingType, queue, resources, urls, position) {
|
||||
this.routingType = routingType;
|
||||
constructor(router, queue, resources, urls, position) {
|
||||
this.router = router;
|
||||
this.queue = queue;
|
||||
this.urls = urls;
|
||||
this.resources = resources;
|
||||
|
@ -29,9 +27,9 @@ class UrlGenerator {
|
|||
|
||||
debug('constructor', this.toString());
|
||||
|
||||
// CASE: channels can define custom filters, but not required.
|
||||
if (this.routingType.getFilter()) {
|
||||
this.filter = transformFilter(this.routingType.getFilter());
|
||||
// CASE: routers can define custom filters, but not required.
|
||||
if (this.router.getFilter()) {
|
||||
this.filter = transformFilter(this.router.getFilter());
|
||||
debug('filter', this.filter);
|
||||
}
|
||||
|
||||
|
@ -43,7 +41,7 @@ class UrlGenerator {
|
|||
* @NOTE: currently only used if the permalink setting changes and it's used for this url generator.
|
||||
* @TODO: remove in Ghost 2.0
|
||||
*/
|
||||
this.routingType.addListener('updated', () => {
|
||||
this.router.addListener('updated', () => {
|
||||
const myResources = this.urls.getByGeneratorId(this.uid);
|
||||
|
||||
myResources.forEach((object) => {
|
||||
|
@ -74,7 +72,7 @@ class UrlGenerator {
|
|||
debug('_onInit', this.toString());
|
||||
|
||||
// @NOTE: get the resources of my type e.g. posts.
|
||||
const resources = this.resources.getAllByType(this.routingType.getType());
|
||||
const resources = this.resources.getAllByType(this.router.getType());
|
||||
|
||||
_.each(resources, (resource) => {
|
||||
this._try(resource);
|
||||
|
@ -85,7 +83,7 @@ class UrlGenerator {
|
|||
debug('onAdded', this.toString());
|
||||
|
||||
// CASE: you are type "pages", but the incoming type is "users"
|
||||
if (event.type !== this.routingType.getType()) {
|
||||
if (event.type !== this.router.getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -138,57 +136,10 @@ class UrlGenerator {
|
|||
* We currently generate relative urls.
|
||||
*/
|
||||
_generateUrl(resource) {
|
||||
let url = this.routingType.getPermalinks().getValue();
|
||||
url = this._replacePermalink(url, resource);
|
||||
const permalink = this.router.getPermalinks().getValue();
|
||||
const url = localUtils.replacePermalink(permalink, resource.data);
|
||||
|
||||
return localUtils.createUrl(url, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO:
|
||||
* This is a copy of `replacePermalink` of our url utility, see ./utils.
|
||||
* But it has modifications, because the whole url utility doesn't work anymore.
|
||||
* We will rewrite some of the functions in the utility.
|
||||
*/
|
||||
_replacePermalink(url, resource) {
|
||||
var output = url,
|
||||
primaryTagFallback = 'all',
|
||||
publishedAtMoment = moment.tz(resource.data.published_at || Date.now(), settingsCache.get('active_timezone')),
|
||||
permalink = {
|
||||
year: function () {
|
||||
return publishedAtMoment.format('YYYY');
|
||||
},
|
||||
month: function () {
|
||||
return publishedAtMoment.format('MM');
|
||||
},
|
||||
day: function () {
|
||||
return publishedAtMoment.format('DD');
|
||||
},
|
||||
author: function () {
|
||||
return resource.data.primary_author.slug;
|
||||
},
|
||||
primary_author: function () {
|
||||
return resource.data.primary_author ? resource.data.primary_author.slug : primaryTagFallback;
|
||||
},
|
||||
primary_tag: function () {
|
||||
return resource.data.primary_tag ? resource.data.primary_tag.slug : primaryTagFallback;
|
||||
},
|
||||
slug: function () {
|
||||
return resource.data.slug;
|
||||
},
|
||||
id: function () {
|
||||
return resource.data.id;
|
||||
}
|
||||
};
|
||||
|
||||
// replace tags like :slug or :year with actual values
|
||||
output = output.replace(/(:[a-z_]+)/g, function (match) {
|
||||
if (_.has(permalink, match.substr(1))) {
|
||||
return permalink[match.substr(1)]();
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
return localUtils.createUrl(url, false, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -218,7 +169,7 @@ class UrlGenerator {
|
|||
action: 'added:' + resource.data.id,
|
||||
eventData: {
|
||||
id: resource.data.id,
|
||||
type: this.routingType.getType()
|
||||
type: this.router.getType()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -234,12 +185,22 @@ class UrlGenerator {
|
|||
resource.addListener('removed', onRemoved.bind(this));
|
||||
}
|
||||
|
||||
hasUrl(url) {
|
||||
const existingUrl = this.urls.getByUrl(url);
|
||||
|
||||
if (existingUrl.length && existingUrl[0].generatorId === this.uid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getUrls() {
|
||||
return this.urls.getByGeneratorId(this.uid);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.routingType.toString();
|
||||
return this.router.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,16 +9,9 @@ const _debug = require('ghost-ignition').debug._base,
|
|||
localUtils = require('./utils');
|
||||
|
||||
class UrlService {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
|
||||
constructor() {
|
||||
this.utils = localUtils;
|
||||
|
||||
// You can disable the url preload, in case we encounter a problem with the new url service.
|
||||
if (options.disableUrlPreload) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.finished = false;
|
||||
this.urlGenerators = [];
|
||||
|
||||
|
@ -31,10 +24,10 @@ class UrlService {
|
|||
|
||||
_listeners() {
|
||||
/**
|
||||
* The purpose of this event is to notify the url service as soon as a channel get's created.
|
||||
* The purpose of this event is to notify the url service as soon as a router get's created.
|
||||
*/
|
||||
this._onRoutingTypeListener = this._onRoutingType.bind(this);
|
||||
common.events.on('routingType.created', this._onRoutingTypeListener);
|
||||
this._onRouterAddedListener = this._onRouterAddedType.bind(this);
|
||||
common.events.on('router.created', this._onRouterAddedListener);
|
||||
|
||||
/**
|
||||
* The queue will notify us if url generation has started/finished.
|
||||
|
@ -44,9 +37,6 @@ class UrlService {
|
|||
|
||||
this._onQueueEndedListener = this._onQueueEnded.bind(this);
|
||||
this.queue.addListener('ended', this._onQueueEnded.bind(this));
|
||||
|
||||
this._resetListener = this.reset.bind(this);
|
||||
common.events.on('server.stop', this._resetListener);
|
||||
}
|
||||
|
||||
_onQueueStarted(event) {
|
||||
|
@ -61,21 +51,42 @@ class UrlService {
|
|||
}
|
||||
}
|
||||
|
||||
_onRoutingType(routingType) {
|
||||
debug('routingType.created');
|
||||
_onRouterAddedType(router) {
|
||||
// CASE: there are router types which do not generate resource urls
|
||||
// e.g. static route router
|
||||
// we are listening on the general `router.created` event - every router throws this event
|
||||
if (!router || !router.getPermalinks()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let urlGenerator = new UrlGenerator(routingType, this.queue, this.resources, this.urls, this.urlGenerators.length);
|
||||
debug('router.created');
|
||||
|
||||
let urlGenerator = new UrlGenerator(router, this.queue, this.resources, this.urls, this.urlGenerators.length);
|
||||
this.urlGenerators.push(urlGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* You have a url and want to know which the url belongs to.
|
||||
*
|
||||
* It's in theory possible that multiple resources generate the same url,
|
||||
* but they both would serve different content e.g. static pages and collections.
|
||||
* but they both would serve different content.
|
||||
*
|
||||
* e.g. if we remove the slug uniqueness and you create a static
|
||||
* page and a post with the same slug. And both are served under `/` with the permalink `/:slug/`.
|
||||
*
|
||||
*
|
||||
* Each url is unique and it depends on the hierarchy of router registration is configured.
|
||||
* There is no url collision, everything depends on registration order.
|
||||
*
|
||||
* e.g. posts which live in a collection are stronger than a static page.
|
||||
*
|
||||
* We only return the resource, which would be served.
|
||||
*
|
||||
* @NOTE: only accepts relative urls at the moment.
|
||||
*/
|
||||
getResource(url) {
|
||||
getResource(url, options) {
|
||||
options = options || {};
|
||||
|
||||
let objects = this.urls.getByUrl(url);
|
||||
|
||||
if (!objects.length) {
|
||||
|
@ -102,9 +113,15 @@ class UrlService {
|
|||
toReturn.push(object);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (options.returnEverything) {
|
||||
return objects[0];
|
||||
}
|
||||
|
||||
return objects[0].resource;
|
||||
}
|
||||
|
||||
|
@ -114,18 +131,71 @@ class UrlService {
|
|||
|
||||
/**
|
||||
* Get url by resource id.
|
||||
* e.g. tags, authors, posts, pages
|
||||
*
|
||||
* If we can't find a url for an id, we have to return a url.
|
||||
* There are many components in Ghost which call `getUrlByResourceId` and
|
||||
* based on the return value, they set the resource url somewhere e.g. meta data.
|
||||
* Or if you define no collections in your yaml file and serve a page.
|
||||
* You will see a suggestion of posts, but they all don't belong to a collection.
|
||||
* They would show localhost:2368/null/.
|
||||
*/
|
||||
getUrlByResourceId(id) {
|
||||
getUrlByResourceId(id, options) {
|
||||
options = options || {};
|
||||
|
||||
const obj = this.urls.getByResourceId(id);
|
||||
|
||||
if (obj) {
|
||||
if (options.absolute) {
|
||||
return this.utils.createUrl(obj.url, options.absolute, options.secure);
|
||||
}
|
||||
|
||||
return obj.url;
|
||||
}
|
||||
|
||||
return null;
|
||||
if (options.absolute) {
|
||||
return this.utils.createUrl('/404/', options.absolute, options.secure);
|
||||
}
|
||||
|
||||
return '/404/';
|
||||
}
|
||||
|
||||
owns(routerId, url) {
|
||||
debug('owns', routerId, url);
|
||||
|
||||
let urlGenerator;
|
||||
|
||||
this.urlGenerators.every((_urlGenerator) => {
|
||||
if (_urlGenerator.router.identifier === routerId) {
|
||||
urlGenerator = _urlGenerator;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!urlGenerator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return urlGenerator.hasUrl(url);
|
||||
}
|
||||
|
||||
getPermalinkByUrl(url, options) {
|
||||
options = options || {};
|
||||
|
||||
const object = this.getResource(url, {returnEverything: true});
|
||||
|
||||
if (!object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _.find(this.urlGenerators, {uid: object.generatorId}).router.getPermalinks()
|
||||
.getValue(options);
|
||||
}
|
||||
|
||||
reset() {
|
||||
debug('reset');
|
||||
this.urlGenerators = [];
|
||||
|
||||
this.urls.reset();
|
||||
|
@ -134,8 +204,24 @@ class UrlService {
|
|||
|
||||
this._onQueueStartedListener && this.queue.removeListener('started', this._onQueueStartedListener);
|
||||
this._onQueueEndedListener && this.queue.removeListener('ended', this._onQueueEndedListener);
|
||||
this._onRoutingTypeListener && common.events.removeListener('routingType.created', this._onRoutingTypeListener);
|
||||
this._resetListener && common.events.removeListener('server.stop', this._resetListener);
|
||||
this._onRouterAddedListener && common.events.removeListener('router.created', this._onRouterAddedListener);
|
||||
}
|
||||
|
||||
resetGenerators() {
|
||||
debug('resetGenerators');
|
||||
this.finished = false;
|
||||
this.urlGenerators = [];
|
||||
this.urls.reset();
|
||||
this.queue.reset();
|
||||
this.resources.softReset();
|
||||
}
|
||||
|
||||
softReset() {
|
||||
debug('softReset');
|
||||
this.finished = false;
|
||||
this.urls.softReset();
|
||||
this.queue.softReset();
|
||||
this.resources.softReset();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,8 @@ class Urls {
|
|||
* - resource1 -> /welcome/
|
||||
* - resource2 -> /welcome/
|
||||
*
|
||||
* But depending on the routing registration, you will always serve e.g. resource1.
|
||||
* But depending on the routing registration, you will always serve e.g. resource1,
|
||||
* because the router it belongs to was registered first.
|
||||
*/
|
||||
getByUrl(url) {
|
||||
return _.reduce(Object.keys(this.urls), (toReturn, resourceId) => {
|
||||
|
@ -102,6 +103,10 @@ class Urls {
|
|||
reset() {
|
||||
this.urls = {};
|
||||
}
|
||||
|
||||
softReset() {
|
||||
this.urls = {};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Urls;
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
const config = require('../../config'),
|
||||
UrlService = require('./UrlService'),
|
||||
urlService = new UrlService({
|
||||
disableUrlPreload: config.get('disableUrlPreload')
|
||||
});
|
||||
const UrlService = require('./UrlService'),
|
||||
urlService = new UrlService();
|
||||
|
||||
// Singleton
|
||||
module.exports = urlService;
|
||||
|
|
|
@ -149,7 +149,7 @@ function getAdminUrl() {
|
|||
// - secure (optional, default:false) - boolean whether or not to force SSL
|
||||
// Returns:
|
||||
// - a URL which always ends with a slash
|
||||
function createUrl(urlPath, absolute, secure) {
|
||||
function createUrl(urlPath, absolute, secure, trailingSlash) {
|
||||
urlPath = urlPath || '/';
|
||||
absolute = absolute || false;
|
||||
var base;
|
||||
|
@ -161,24 +161,23 @@ function createUrl(urlPath, absolute, secure) {
|
|||
base = getSubdir();
|
||||
}
|
||||
|
||||
if (trailingSlash) {
|
||||
if (!urlPath.match(/\/$/)) {
|
||||
urlPath += '/';
|
||||
}
|
||||
}
|
||||
|
||||
return urlJoin(base, urlPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* creates the url path for a post based on blog timezone and permalink pattern
|
||||
*
|
||||
* @param {JSON} post
|
||||
* @returns {string}
|
||||
*/
|
||||
function urlPathForPost(post) {
|
||||
var output = '',
|
||||
permalinks = settingsCache.get('permalinks'),
|
||||
// routeKeywords.primaryTagFallback: 'all'
|
||||
function replacePermalink(permalink, resource) {
|
||||
let output = permalink,
|
||||
primaryTagFallback = 'all',
|
||||
// routeKeywords.primaryAuthorFallback: 'all'
|
||||
primaryAuthorFallback = 'all',
|
||||
publishedAtMoment = moment.tz(post.published_at || Date.now(), settingsCache.get('active_timezone')),
|
||||
tags = {
|
||||
publishedAtMoment = moment.tz(resource.published_at || Date.now(), settingsCache.get('active_timezone')),
|
||||
permalinkLookUp = {
|
||||
year: function () {
|
||||
return publishedAtMoment.format('YYYY');
|
||||
},
|
||||
|
@ -188,36 +187,27 @@ function urlPathForPost(post) {
|
|||
day: function () {
|
||||
return publishedAtMoment.format('DD');
|
||||
},
|
||||
/**
|
||||
* @deprecated: `author`, will be removed in Ghost 2.0
|
||||
*/
|
||||
author: function () {
|
||||
return post.author.slug;
|
||||
},
|
||||
primary_tag: function () {
|
||||
return post.primary_tag ? post.primary_tag.slug : primaryTagFallback;
|
||||
return resource.primary_author.slug;
|
||||
},
|
||||
primary_author: function () {
|
||||
return post.primary_author ? post.primary_author.slug : primaryAuthorFallback;
|
||||
return resource.primary_author ? resource.primary_author.slug : primaryTagFallback;
|
||||
},
|
||||
primary_tag: function () {
|
||||
return resource.primary_tag ? resource.primary_tag.slug : primaryTagFallback;
|
||||
},
|
||||
slug: function () {
|
||||
return post.slug;
|
||||
return resource.slug;
|
||||
},
|
||||
id: function () {
|
||||
return post.id;
|
||||
return resource.id;
|
||||
}
|
||||
};
|
||||
|
||||
if (post.page) {
|
||||
output += '/:slug/';
|
||||
} else {
|
||||
output += permalinks;
|
||||
}
|
||||
|
||||
// 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)]();
|
||||
if (_.has(permalinkLookUp, match.substr(1))) {
|
||||
return permalinkLookUp[match.substr(1)]();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -226,17 +216,13 @@ function urlPathForPost(post) {
|
|||
|
||||
// ## urlFor
|
||||
// Synchronous url creation for a given context
|
||||
// Can generate a url for a named path, given path, or known object (post)
|
||||
// Can generate a url for a named path and given path.
|
||||
// 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
|
||||
|
@ -246,13 +232,12 @@ function urlPathForPost(post) {
|
|||
function urlFor(context, data, absolute) {
|
||||
var urlPath = '/',
|
||||
secure, imagePathRe,
|
||||
knownObjects = ['post', 'tag', 'author', 'image', 'nav'], baseUrl,
|
||||
knownObjects = ['image', 'nav'], baseUrl,
|
||||
hostname,
|
||||
|
||||
// this will become really big
|
||||
knownPaths = {
|
||||
home: '/',
|
||||
rss: '/rss/',
|
||||
api: API_PATH,
|
||||
sitemap_xsl: '/sitemap.xsl'
|
||||
};
|
||||
|
@ -269,19 +254,7 @@ function urlFor(context, data, absolute) {
|
|||
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) {
|
||||
urlPath = data.post.url;
|
||||
secure = data.secure;
|
||||
} else if (context === 'tag' && data.tag) {
|
||||
// routeKeywords.tag: 'tag'
|
||||
urlPath = urlJoin('/tag', data.tag.slug, '/');
|
||||
secure = data.tag.secure;
|
||||
} else if (context === 'author' && data.author) {
|
||||
// routeKeywords.author: 'author'
|
||||
urlPath = urlJoin('/author', data.author.slug, '/');
|
||||
secure = data.author.secure;
|
||||
} else if (context === 'image' && data.image) {
|
||||
if (context === 'image' && data.image) {
|
||||
urlPath = data.image;
|
||||
imagePathRe = new RegExp('^' + getSubdir() + '/' + STATIC_IMAGE_URL_PREFIX);
|
||||
absolute = imagePathRe.test(data.image) ? absolute : false;
|
||||
|
@ -435,16 +408,27 @@ function makeAbsoluteUrls(html, siteUrl, itemUrl) {
|
|||
return htmlContent;
|
||||
}
|
||||
|
||||
function absoluteToRelative(urlToModify) {
|
||||
const urlObj = url.parse(urlToModify);
|
||||
return urlObj.pathname;
|
||||
}
|
||||
|
||||
function deduplicateDoubleSlashes(url) {
|
||||
return url.replace(/\/\//g, '/');
|
||||
}
|
||||
|
||||
module.exports.absoluteToRelative = absoluteToRelative;
|
||||
module.exports.makeAbsoluteUrls = makeAbsoluteUrls;
|
||||
module.exports.getProtectedSlugs = getProtectedSlugs;
|
||||
module.exports.getSubdir = getSubdir;
|
||||
module.exports.urlJoin = urlJoin;
|
||||
module.exports.urlFor = urlFor;
|
||||
module.exports.isSSL = isSSL;
|
||||
module.exports.urlPathForPost = urlPathForPost;
|
||||
module.exports.replacePermalink = replacePermalink;
|
||||
module.exports.redirectToAdmin = redirectToAdmin;
|
||||
module.exports.redirect301 = redirect301;
|
||||
module.exports.createUrl = createUrl;
|
||||
module.exports.deduplicateDoubleSlashes = deduplicateDoubleSlashes;
|
||||
|
||||
/**
|
||||
* If you request **any** image in Ghost, it get's served via
|
||||
|
|
|
@ -28,7 +28,7 @@ var _ = require('lodash'),
|
|||
function ping(post) {
|
||||
var pingXML,
|
||||
title = post.title,
|
||||
url = urlService.utils.urlFor('post', {post: post}, true);
|
||||
url = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||
|
||||
if (post.page || config.isPrivacyDisabled('useRpcPing') || settingsCache.get('is_private')) {
|
||||
return;
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
},
|
||||
"general": {
|
||||
"maintenance": "Ghost is currently undergoing maintenance, please wait a moment then retry.",
|
||||
"maintenanceUrlService": "Ghost currently generates your blog urls. Please try again.",
|
||||
"requiredOnFuture": "This will be required in future. Please see {link}",
|
||||
"internalError": "Something went wrong.",
|
||||
"jsonParse": "Could not parse JSON: {context}."
|
||||
|
@ -472,7 +473,8 @@
|
|||
"settings": {
|
||||
"yaml": {
|
||||
"error": "Could not parse {file}: {context}.",
|
||||
"help": "Check your {file} file for typos and fix the named issues."
|
||||
"help": "Check your {file} file for typos and fix the named issues.",
|
||||
"validate": "The following definition \"{at}\" is invalid: {reason}"
|
||||
},
|
||||
"loader": "Error trying to load YAML setting for {setting} from '{path}'.",
|
||||
"ensureSettings": "Error trying to access settings files in {path}."
|
||||
|
|
|
@ -2,7 +2,7 @@ var _ = require('lodash'),
|
|||
hbs = require('express-hbs'),
|
||||
config = require('../../config'),
|
||||
common = require('../../lib/common'),
|
||||
templates = require('../../controllers/frontend/templates'),
|
||||
helpers = require('../../services/routing/helpers'),
|
||||
escapeExpression = hbs.Utils.escapeExpression,
|
||||
_private = {},
|
||||
errorHandler = {};
|
||||
|
@ -98,7 +98,8 @@ _private.ThemeErrorRenderer = function ThemeErrorRenderer(err, req, res, next) {
|
|||
};
|
||||
|
||||
// Template
|
||||
templates.setTemplate(req, res);
|
||||
// @TODO: very dirty !!!!!!
|
||||
helpers.templates.setTemplate(req, res);
|
||||
|
||||
// It can be that something went wrong with the theme or otherwise loading handlebars
|
||||
// This ensures that no matter what res.render will work here
|
||||
|
|
5
core/server/web/middleware/index.js
Normal file
5
core/server/web/middleware/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
get maintenance() {
|
||||
return require('./maintenance');
|
||||
}
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
var config = require('../../config'),
|
||||
common = require('../../lib/common');
|
||||
common = require('../../lib/common'),
|
||||
urlService = require('../../services/url');
|
||||
|
||||
module.exports = function maintenance(req, res, next) {
|
||||
if (config.get('maintenance').enabled) {
|
||||
|
@ -8,5 +9,11 @@ module.exports = function maintenance(req, res, next) {
|
|||
}));
|
||||
}
|
||||
|
||||
if (!urlService.hasFinished()) {
|
||||
return next(new common.errors.MaintenanceError({
|
||||
message: common.i18n.t('errors.general.maintenanceUrlService')
|
||||
}));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
|
|
@ -123,9 +123,6 @@ module.exports = function setupSiteApp() {
|
|||
|
||||
debug('General middleware done');
|
||||
|
||||
// @temporary
|
||||
require('../../services/channels/Channels2');
|
||||
|
||||
// Set up Frontend routes (including private blogging routes)
|
||||
siteApp.use(siteRoutes());
|
||||
|
||||
|
|
|
@ -1,46 +1,5 @@
|
|||
var debug = require('ghost-ignition').debug('site:routes'),
|
||||
|
||||
routeService = require('../../services/route'),
|
||||
siteRouter = routeService.siteRouter,
|
||||
|
||||
// Sub Routers
|
||||
appRouter = routeService.appRouter,
|
||||
channelsService = require('../../services/channels'),
|
||||
|
||||
// Controllers
|
||||
controllers = require('../../controllers'),
|
||||
|
||||
// Utils for creating paths
|
||||
// @TODO: refactor these away
|
||||
urlService = require('../../services/url');
|
||||
const routing = require('../../services/routing');
|
||||
|
||||
module.exports = function siteRoutes() {
|
||||
// @TODO move this path out of this file!
|
||||
// Note this also exists in api/events.js
|
||||
// routeKeywords.preview: 'p'
|
||||
var previewRoute = urlService.utils.urlJoin('/p', ':uuid', ':options?');
|
||||
|
||||
// Preview - register controller as route
|
||||
// Ideal version, as we don't want these paths all over the place
|
||||
// previewRoute = new Route('GET /:t_preview/:uuid/:options?', previewController);
|
||||
// siteRouter.mountRoute(previewRoute);
|
||||
// Orrrrr maybe preview should be an internal App??!
|
||||
siteRouter.mountRoute(previewRoute, controllers.preview);
|
||||
|
||||
// Channels - register sub-router
|
||||
// The purpose of having a parentRouter for channels, is so that we can load channels from wherever we want:
|
||||
// config, settings, apps, etc, and that it will be possible for the router to be reloaded.
|
||||
siteRouter.mountRouter(channelsService.router());
|
||||
|
||||
// Apps - register sub-router
|
||||
// The purpose of having a parentRouter for apps, is that Apps can register a route whenever they want.
|
||||
// Apps cannot yet deregister, it's complex to implement and I don't yet have a clear use-case for this.
|
||||
siteRouter.mountRouter(appRouter.router());
|
||||
|
||||
// Default - register entry controller as route
|
||||
siteRouter.mountRoute('*', controllers.entry);
|
||||
|
||||
debug('Routes:', routeService.registry.getAll());
|
||||
|
||||
return siteRouter.router();
|
||||
return routing.bootstrap();
|
||||
};
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
// # Channel Route Tests
|
||||
// # Dynamic Routing Tests
|
||||
// As it stands, these tests depend on the database, and as such are integration tests.
|
||||
// These tests are here to cover the headers sent with requests and high-level redirects that can't be
|
||||
// tested with the unit tests
|
||||
var should = require('should'),
|
||||
const should = require('should'),
|
||||
supertest = require('supertest'),
|
||||
sinon = require('sinon'),
|
||||
moment = require('moment'),
|
||||
testUtils = require('../../utils'),
|
||||
cheerio = require('cheerio'),
|
||||
config = require('../../../../core/server/config'),
|
||||
urlService = require('../../../../core/server/services/url'),
|
||||
settingsCache = require('../../../server/services/settings/cache'),
|
||||
ghost = testUtils.startGhost,
|
||||
sandbox = sinon.sandbox.create(),
|
||||
request;
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('Channel Routes', function () {
|
||||
let request;
|
||||
|
||||
describe('Dynamic Routing', function () {
|
||||
var ghostServer;
|
||||
|
||||
function doEnd(done) {
|
||||
|
@ -53,7 +56,7 @@ describe('Channel Routes', function () {
|
|||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('Index', function () {
|
||||
describe('Collection Index', function () {
|
||||
it('should respond with html', function (done) {
|
||||
request.get('/')
|
||||
.expect('Content-Type', /html/)
|
||||
|
@ -240,6 +243,34 @@ describe('Channel Routes', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Collection Entry', function () {
|
||||
before(function () {
|
||||
return testUtils.initData().then(function () {
|
||||
return testUtils.fixtures.overrideOwnerUser();
|
||||
}).then(function () {
|
||||
return testUtils.fixtures.insertPostsAndTags();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render page with slug permalink', function (done) {
|
||||
request.get('/static-page-test/')
|
||||
.expect('Content-Type', /html/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||
.expect(200)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should not render page with dated permalink', function (done) {
|
||||
const date = moment().format('YYYY/MM/DD');
|
||||
|
||||
request.get('/' + date + '/static-page-test/')
|
||||
.expect('Content-Type', /html/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tag', function () {
|
||||
before(function (done) {
|
||||
testUtils.clearData().then(function () {
|
||||
|
@ -325,6 +356,32 @@ describe('Channel Routes', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
it('should redirect without slash', function (done) {
|
||||
request.get('/tag/getting-started/edit')
|
||||
.expect('Location', '/tag/getting-started/edit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should redirect to tag settings', function (done) {
|
||||
request.get('/tag/getting-started/edit/')
|
||||
.expect('Location', '/ghost/#/settings/tags/getting-started/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||
.expect(302)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should 404 for non-edit parameter', function (done) {
|
||||
request.get('/tag/getting-started/notedit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.expect(/Page not found/)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Paged', function () {
|
||||
before(testUtils.teardown);
|
||||
|
||||
|
@ -409,32 +466,6 @@ describe('Channel Routes', function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
it('should redirect without slash', function (done) {
|
||||
request.get('/tag/getting-started/edit')
|
||||
.expect('Location', '/tag/getting-started/edit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should redirect to tag settings', function (done) {
|
||||
request.get('/tag/getting-started/edit/')
|
||||
.expect('Location', '/ghost/#/settings/tags/getting-started/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||
.expect(302)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should 404 for non-edit parameter', function (done) {
|
||||
request.get('/tag/getting-started/notedit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.expect(/Page not found/)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Author', function () {
|
||||
|
@ -549,6 +580,32 @@ describe('Channel Routes', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
it('should redirect without slash', function (done) {
|
||||
request.get('/author/ghost-owner/edit')
|
||||
.expect('Location', '/author/ghost-owner/edit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should redirect to editor', function (done) {
|
||||
request.get('/author/ghost-owner/edit/')
|
||||
.expect('Location', '/ghost/#/team/ghost-owner/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||
.expect(302)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should 404 for something that isn\'t edit', function (done) {
|
||||
request.get('/author/ghost-owner/notedit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.expect(/Page not found/)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Paged', function () {
|
||||
// Add enough posts to trigger pages
|
||||
before(function (done) {
|
||||
|
@ -634,31 +691,5 @@ describe('Channel Routes', function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
it('should redirect without slash', function (done) {
|
||||
request.get('/author/ghost-owner/edit')
|
||||
.expect('Location', '/author/ghost-owner/edit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.year)
|
||||
.expect(301)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should redirect to editor', function (done) {
|
||||
request.get('/author/ghost-owner/edit/')
|
||||
.expect('Location', '/ghost/#/team/ghost-owner/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.public)
|
||||
.expect(302)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
|
||||
it('should 404 for something that isn\'t edit', function (done) {
|
||||
request.get('/author/ghost-owner/notedit/')
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.expect(/Page not found/)
|
||||
.end(doEnd(done));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -214,6 +214,7 @@ describe('Frontend Routing', function () {
|
|||
should.not.exist(res.headers['set-cookie']);
|
||||
should.exist(res.headers.date);
|
||||
|
||||
// NOTE: This is the title from the settings.
|
||||
$('title').text().should.equal('Welcome to Ghost');
|
||||
|
||||
// @TODO: change or remove?
|
||||
|
@ -301,6 +302,7 @@ describe('Frontend Routing', function () {
|
|||
should.exist(res.headers.date);
|
||||
|
||||
$('title').text().should.equal('Welcome to Ghost');
|
||||
|
||||
$('.content .post').length.should.equal(1);
|
||||
$('.poweredby').text().should.equal('Proudly published with Ghost');
|
||||
$('body.amp-template').length.should.equal(1);
|
||||
|
|
|
@ -9,6 +9,7 @@ var should = require('should'),
|
|||
db = require('../../../server/data/db'),
|
||||
models = require('../../../server/models'),
|
||||
PostAPI = require('../../../server/api/posts'),
|
||||
urlService = require('../../../server/services/url'),
|
||||
settingsCache = require('../../../server/services/settings/cache'),
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
@ -422,6 +423,8 @@ describe('Post API', function () {
|
|||
});
|
||||
|
||||
it('with context.user can fetch url and author fields', function (done) {
|
||||
sandbox.stub(urlService, 'getUrlByResourceId').withArgs(testUtils.DataGenerator.Content.posts[7].id).returns('/html-ipsum/');
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', limit: 5}).then(function (results) {
|
||||
should.exist(results.posts);
|
||||
|
||||
|
|
|
@ -220,7 +220,11 @@ describe('Import', function () {
|
|||
testUtils.fixtures.loadExportFixture('export-001', {lts: true}).then(function (exported) {
|
||||
exportData = exported;
|
||||
return dataImporter.doImport(exportData, importOptions);
|
||||
}).then(function () {
|
||||
}).then(function (result) {
|
||||
// NOTE: the user in the JSON is not imported (!) - duplicate email
|
||||
result.problems[1].help.should.eql('User');
|
||||
result.problems[1].message.should.eql('Entry was not imported and ignored. Detected duplicated entry.');
|
||||
|
||||
// Grab the data from tables
|
||||
return Promise.all([
|
||||
knex('users').select(),
|
||||
|
@ -248,10 +252,9 @@ describe('Import', function () {
|
|||
hashedPassword: users[0].password,
|
||||
plainPassword: testUtils.DataGenerator.Content.users[0].password
|
||||
}).then(function () {
|
||||
// but the name, slug, and bio should have been overridden
|
||||
users[0].name.should.equal(exportData.data.users[0].name);
|
||||
users[0].slug.should.equal(exportData.data.users[0].slug);
|
||||
should.not.exist(users[0].bio, 'bio is not imported');
|
||||
users[0].email.should.equal(exportData.data.users[0].email);
|
||||
|
||||
// import no longer requires all data to be dropped, and adds posts
|
||||
posts.length.should.equal(exportData.data.posts.length, 'Wrong number of posts');
|
||||
|
@ -323,7 +326,11 @@ describe('Import', function () {
|
|||
testUtils.fixtures.loadExportFixture('export-002', {lts: true}).then(function (exported) {
|
||||
exportData = exported;
|
||||
return dataImporter.doImport(exportData, importOptions);
|
||||
}).then(function () {
|
||||
}).then(function (result) {
|
||||
// NOTE: the user in the JSON is not imported (!) - duplicate email
|
||||
result.problems[1].help.should.eql('User');
|
||||
result.problems[1].message.should.eql('Entry was not imported and ignored. Detected duplicated entry.');
|
||||
|
||||
// Grab the data from tables
|
||||
return Promise.all([
|
||||
knex('users').select(),
|
||||
|
@ -351,10 +358,9 @@ describe('Import', function () {
|
|||
hashedPassword: users[0].password,
|
||||
plainPassword: testUtils.DataGenerator.Content.users[0].password
|
||||
}).then(function () {
|
||||
// but the name, slug, and bio should have been overridden
|
||||
users[0].name.should.equal(exportData.data.users[0].name);
|
||||
users[0].slug.should.equal(exportData.data.users[0].slug);
|
||||
should.not.exist(users[0].bio, 'bio is not imported');
|
||||
users[0].email.should.equal(exportData.data.users[0].email);
|
||||
|
||||
// import no longer requires all data to be dropped, and adds posts
|
||||
posts.length.should.equal(exportData.data.posts.length, 'Wrong number of posts');
|
||||
|
@ -390,7 +396,11 @@ describe('Import', function () {
|
|||
testUtils.fixtures.loadExportFixture('export-003', {lts: true}).then(function (exported) {
|
||||
exportData = exported;
|
||||
return dataImporter.doImport(exportData, importOptions);
|
||||
}).then(function () {
|
||||
}).then(function (result) {
|
||||
// NOTE: the user in the JSON is not imported (!) - duplicate email
|
||||
result.problems[1].help.should.eql('User');
|
||||
result.problems[1].message.should.eql('Entry was not imported and ignored. Detected duplicated entry.');
|
||||
|
||||
// Grab the data from tables
|
||||
return Promise.all([
|
||||
knex('users').select(),
|
||||
|
@ -413,10 +423,9 @@ describe('Import', function () {
|
|||
hashedPassword: users[0].password,
|
||||
plainPassword: testUtils.DataGenerator.Content.users[0].password
|
||||
}).then(function () {
|
||||
// but the name, slug, and bio should have been overridden
|
||||
users[0].name.should.equal(exportData.data.users[0].name);
|
||||
users[0].slug.should.equal(exportData.data.users[0].slug);
|
||||
should.not.exist(users[0].bio, 'bio is not imported');
|
||||
users[0].email.should.equal(exportData.data.users[0].email);
|
||||
|
||||
// test posts
|
||||
posts.length.should.equal(1, 'Wrong number of posts');
|
||||
|
@ -521,10 +530,8 @@ describe('Import', function () {
|
|||
hashedPassword: users[0].password,
|
||||
plainPassword: testUtils.DataGenerator.Content.users[0].password
|
||||
}).then(function () {
|
||||
// but the name, slug, and bio should have been overridden
|
||||
users[0].name.should.equal('Joe Bloggs');
|
||||
users[0].slug.should.equal('joe-bloggs');
|
||||
should.not.exist(users[0].bio, 'bio is not imported');
|
||||
|
||||
// test posts
|
||||
posts.length.should.equal(1, 'Wrong number of posts');
|
||||
|
|
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue