0
Fork 0
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:
kirrg001 2018-06-21 15:46:42 +02:00
parent 15a85add57
commit 0046dce39f
12 changed files with 237 additions and 65 deletions

View file

@ -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')
};

View file

@ -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')
};

View file

@ -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')
};

View file

@ -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';

View file

@ -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'
};

View file

@ -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, {

View file

@ -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;

View file

@ -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']
}
}
});

View file

@ -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);

View 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: []});
});
});
});

View file

@ -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'
};

View file

@ -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']
}
}
});
});
});
});