mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Dynamic Routing Beta: Better template support
refs #9601 - single or multiple template definition - possible formats: ``` routes: /about/: about ``` ``` routes: /about/: template: about ``` ``` routes: /about/: template: - about - me ``` ``` collections /posts/: template: - posts - general ``` ``` collections /posts/: template: posts ```
This commit is contained in:
parent
15a85add57
commit
0046dce39f
12 changed files with 237 additions and 65 deletions
|
@ -13,7 +13,7 @@ function _renderer(req, res, next) {
|
|||
// @TODO refactor into to something explicit & DRY this up
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: templateName,
|
||||
templates: templateName,
|
||||
defaultTemplate: path.resolve(__dirname, 'views', templateName + '.hbs')
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ function _renderer(req, res) {
|
|||
// @TODO refactor into to something explicit & DRY this up
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: templateName,
|
||||
templates: templateName,
|
||||
defaultTemplate: path.resolve(__dirname, 'views', templateName + '.hbs')
|
||||
};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function _renderer(req, res) {
|
|||
// @TODO refactor into to something explicit & DRY this up
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: templateName,
|
||||
templates: templateName,
|
||||
defaultTemplate: path.resolve(__dirname, 'views', templateName + '.hbs')
|
||||
};
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class CollectionRouter extends ParentRouter {
|
|||
};
|
||||
|
||||
// @NOTE: see helpers/templates - we use unshift to prepend the templates
|
||||
this.templates = (object.template || []).reverse();
|
||||
this.templates = (object.templates || []).reverse();
|
||||
|
||||
this.filter = object.filter || 'page:false';
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ const helpers = require('./helpers');
|
|||
const ParentRouter = require('./ParentRouter');
|
||||
|
||||
class StaticRoutesRouter extends ParentRouter {
|
||||
constructor(key, template) {
|
||||
constructor(key, object) {
|
||||
super('StaticRoutesRouter');
|
||||
|
||||
this.route = {value: key};
|
||||
this.template = template;
|
||||
this.templates = object.templates || [];
|
||||
|
||||
debug(this.route.value, this.template);
|
||||
debug(this.route.value, this.templates);
|
||||
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
@ -24,9 +24,10 @@ class StaticRoutesRouter extends ParentRouter {
|
|||
}
|
||||
|
||||
_prepareContext(req, res, next) {
|
||||
// @TODO: index.hbs as fallback for static routes O_O
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: this.template,
|
||||
templates: this.templates,
|
||||
defaultTemplate: 'index'
|
||||
};
|
||||
|
||||
|
|
|
@ -171,7 +171,7 @@ module.exports.setTemplate = function setTemplate(req, res, data) {
|
|||
|
||||
switch (routeConfig.type) {
|
||||
case 'custom':
|
||||
res._template = _private.pickTemplate(routeConfig.templateName, routeConfig.defaultTemplate);
|
||||
res._template = _private.pickTemplate(routeConfig.templates, routeConfig.defaultTemplate);
|
||||
break;
|
||||
case 'collection':
|
||||
res._template = _private.getTemplateForCollection(res.locals.routerOptions, {
|
||||
|
|
|
@ -2,6 +2,29 @@ const _ = require('lodash');
|
|||
const common = require('../../lib/common');
|
||||
const _private = {};
|
||||
|
||||
_private.validateTemplate = function validateTemplate(object) {
|
||||
// CASE: /about/: about
|
||||
if (typeof object === 'string') {
|
||||
return {
|
||||
templates: [object]
|
||||
};
|
||||
}
|
||||
|
||||
if (!object.hasOwnProperty('template')) {
|
||||
object.templates = [];
|
||||
return object;
|
||||
}
|
||||
|
||||
if (_.isArray(object.template)) {
|
||||
object.templates = object.template;
|
||||
} else {
|
||||
object.templates = [object.template];
|
||||
}
|
||||
|
||||
delete object.template;
|
||||
return object;
|
||||
};
|
||||
|
||||
_private.validateRoutes = function validateRoutes(routes) {
|
||||
_.each(routes, (routingTypeObject, routingTypeObjectKey) => {
|
||||
// CASE: we hard-require trailing slashes for the index route
|
||||
|
@ -34,6 +57,8 @@ _private.validateRoutes = function validateRoutes(routes) {
|
|||
help: 'e.g. permalink: /{slug}/'
|
||||
});
|
||||
}
|
||||
|
||||
routes[routingTypeObjectKey] = _private.validateTemplate(routingTypeObject);
|
||||
});
|
||||
|
||||
return routes;
|
||||
|
@ -72,53 +97,54 @@ _private.validateCollections = function validateCollections(collections) {
|
|||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
collections[routingTypeObjectKey] = _private.validateTemplate(routingTypeObject);
|
||||
});
|
||||
|
||||
return collections;
|
||||
|
|
|
@ -722,7 +722,7 @@ describe('Integration - Web - Site', function () {
|
|||
collections: {
|
||||
'/': {
|
||||
permalink: '/:slug/',
|
||||
template: ['default']
|
||||
templates: ['default']
|
||||
},
|
||||
'/magic/': {
|
||||
permalink: '/magic/:slug/'
|
||||
|
@ -792,7 +792,7 @@ describe('Integration - Web - Site', function () {
|
|||
collections: {
|
||||
'/': {
|
||||
permalink: '/:slug/',
|
||||
template: ['something', 'default']
|
||||
templates: ['something', 'default']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -844,11 +844,11 @@ describe('Integration - Web - Site', function () {
|
|||
collections: {
|
||||
'/': {
|
||||
permalink: '/:slug/',
|
||||
template: ['something', 'default']
|
||||
templates: ['something', 'default']
|
||||
},
|
||||
'/magic/': {
|
||||
permalink: '/magic/:slug/',
|
||||
template: ['something', 'default']
|
||||
templates: ['something', 'default']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -129,7 +129,7 @@ describe('UNIT - services/routing/CollectionRouter', function () {
|
|||
});
|
||||
|
||||
it('with templates', function () {
|
||||
const collectionRouter = new CollectionRouter('/magic/', {permalink: '/:slug/', template: ['home', 'index']});
|
||||
const collectionRouter = new CollectionRouter('/magic/', {permalink: '/:slug/', templates: ['home', 'index']});
|
||||
|
||||
// they are getting reversed because we unshift the templates in the helper
|
||||
collectionRouter.templates.should.eql(['index', 'home']);
|
||||
|
@ -138,7 +138,7 @@ describe('UNIT - services/routing/CollectionRouter', function () {
|
|||
|
||||
describe('fn: _prepareIndexContext', function () {
|
||||
it('default', function () {
|
||||
const collectionRouter = new CollectionRouter('/magic/', {permalink: '/:slug/', template: ['home', 'index']});
|
||||
const collectionRouter = new CollectionRouter('/magic/', {permalink: '/:slug/', templates: ['home', 'index']});
|
||||
|
||||
collectionRouter._prepareIndexContext(req, res, next);
|
||||
|
||||
|
|
70
core/test/unit/services/routing/StaticRoutesRouter_spec.js
Normal file
70
core/test/unit/services/routing/StaticRoutesRouter_spec.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
const should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
settingsCache = require('../../../../server/services/settings/cache'),
|
||||
common = require('../../../../server/lib/common'),
|
||||
StaticRoutesRouter = require('../../../../server/services/routing/StaticRoutesRouter'),
|
||||
configUtils = require('../../../utils/configUtils'),
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
||||
describe('UNIT - services/routing/StaticRoutesRouter', function () {
|
||||
let req, res, next;
|
||||
|
||||
afterEach(function () {
|
||||
configUtils.restore();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
sandbox.stub(settingsCache, 'get').withArgs('permalinks').returns('/:slug/');
|
||||
|
||||
sandbox.stub(common.events, 'emit');
|
||||
sandbox.stub(common.events, 'on');
|
||||
|
||||
sandbox.spy(StaticRoutesRouter.prototype, 'mountRoute');
|
||||
sandbox.spy(StaticRoutesRouter.prototype, 'mountRouter');
|
||||
|
||||
req = sandbox.stub();
|
||||
res = sandbox.stub();
|
||||
next = sandbox.stub();
|
||||
|
||||
res.locals = {};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('static routes', function () {
|
||||
it('instantiate: default', function () {
|
||||
const staticRoutesRouter = new StaticRoutesRouter('/about/', {templates: ['test']});
|
||||
should.exist(staticRoutesRouter.router);
|
||||
|
||||
should.not.exist(staticRoutesRouter.getFilter());
|
||||
should.not.exist(staticRoutesRouter.getPermalinks());
|
||||
|
||||
staticRoutesRouter.templates.should.eql(['test']);
|
||||
|
||||
common.events.emit.calledOnce.should.be.true();
|
||||
common.events.emit.calledWith('router.created', staticRoutesRouter).should.be.true();
|
||||
|
||||
staticRoutesRouter.mountRoute.callCount.should.eql(1);
|
||||
|
||||
// parent route
|
||||
staticRoutesRouter.mountRoute.args[0][0].should.eql('/about/');
|
||||
staticRoutesRouter.mountRoute.args[0][1].should.eql(staticRoutesRouter._renderStaticRoute.bind(staticRoutesRouter));
|
||||
});
|
||||
|
||||
it('fn: _prepareContext', function () {
|
||||
const staticRoutesRouter = new StaticRoutesRouter('/about/', {templates: []});
|
||||
|
||||
staticRoutesRouter._prepareContext(req, res, next);
|
||||
next.called.should.be.true();
|
||||
res._route.should.eql({
|
||||
type: 'custom',
|
||||
templates: [],
|
||||
defaultTemplate: 'index'
|
||||
});
|
||||
|
||||
res.locals.routerOptions.should.eql({context: []});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -423,7 +423,7 @@ describe('templates', function () {
|
|||
it('calls pickTemplate for custom routes', function () {
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: 'test',
|
||||
templates: 'test',
|
||||
defaultTemplate: 'path/to/local/test.hbs'
|
||||
};
|
||||
|
||||
|
@ -445,7 +445,7 @@ describe('templates', function () {
|
|||
it('calls pickTemplate for custom routes', function () {
|
||||
res._route = {
|
||||
type: 'custom',
|
||||
templateName: 'test',
|
||||
templates: 'test',
|
||||
defaultTemplate: 'path/to/local/test.hbs'
|
||||
};
|
||||
|
||||
|
|
|
@ -205,7 +205,8 @@ describe('UNIT: services/settings/validate', function () {
|
|||
routes: {},
|
||||
collections: {
|
||||
'/magic/': {
|
||||
permalink: '{globals.permalinks}'
|
||||
permalink: '{globals.permalinks}',
|
||||
templates: []
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -253,12 +254,86 @@ describe('UNIT: services/settings/validate', function () {
|
|||
},
|
||||
collections: {
|
||||
'/magic/': {
|
||||
permalink: '/magic/:year/:slug/'
|
||||
permalink: '/magic/:year/:slug/',
|
||||
templates: []
|
||||
},
|
||||
'/': {
|
||||
permalink: '/:slug/'
|
||||
permalink: '/:slug/',
|
||||
templates: []
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('template definitions', function () {
|
||||
it('single value', function () {
|
||||
const object = validate({
|
||||
routes: {
|
||||
'/about/': 'about',
|
||||
'/me/': {
|
||||
template: 'me'
|
||||
}
|
||||
},
|
||||
collections: {
|
||||
'/': {
|
||||
permalink: '/{slug}/',
|
||||
template: 'test'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
object.should.eql({
|
||||
taxonomies: {},
|
||||
routes: {
|
||||
'/about/': {
|
||||
templates: ['about']
|
||||
},
|
||||
'/me/': {
|
||||
templates: ['me']
|
||||
}
|
||||
},
|
||||
collections: {
|
||||
'/': {
|
||||
permalink: '/:slug/',
|
||||
templates: ['test']
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('array', function () {
|
||||
const object = validate({
|
||||
routes: {
|
||||
'/about/': 'about',
|
||||
'/me/': {
|
||||
template: ['me']
|
||||
}
|
||||
},
|
||||
collections: {
|
||||
'/': {
|
||||
permalink: '/{slug}/',
|
||||
template: ['test']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
object.should.eql({
|
||||
taxonomies: {},
|
||||
routes: {
|
||||
'/about/': {
|
||||
templates: ['about']
|
||||
},
|
||||
'/me/': {
|
||||
templates: ['me']
|
||||
}
|
||||
},
|
||||
collections: {
|
||||
'/': {
|
||||
permalink: '/:slug/',
|
||||
templates: ['test']
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue