- Reference Implementation Introduction
- Routing
- Rendering
███████╗ █████╗ ██╗ ██╗ ██████╗██╗███████╗██████╗
██╔════╝██╔══██╗██║ ██║██╔════╝██║██╔════╝██╔══██╗
███████╗███████║██║ ██║██║ ██║█████╗ ██████╔╝
╚════██║██╔══██║██║ ██║██║ ██║██╔══╝ ██╔══██╗
███████║██║ ██║╚██████╔╝╚██████╗██║███████╗██║ ██║
╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚═╝╚══════╝╚═╝ ╚═╝
- Place to explore ideas about this concept.
- Consistent starting point.
- Solve hard problems once, maybe.
- Maintenance
- Create an
./config/env.json
file. A template is located at,./config/_env.json
- Ensure the
local
environment in./config/env.json
is populated with- The API endpoint you want to operate against.
- The Redis instance you want to get/set information from.
- Create an
./config/_secrets.json
file. A template is located at,./config/secrets.json
. - Populate the
./config/secrets.json
file.
{
"wwwSecret": "1wo7kwewmi",
"apiSecret": "48eusaif6r",
"routeCacheSecret": "pb89yxy9zfr"
}
Breakup your route definitions into logical defaults. Something to remember is that routes are processed in order. Additionally all routes attached in app.js
are processed after anything attached in headless.js
.
routesWeb = require('./routes/web')(headlessDrupal);
/**
* Home
*/
app.routeMulti('/', 'content/home.dust', {
resource: [
'foo?bar=1',
'foo?baz=3'
],
processors: ['homepage', 'googleAnalytics']
});
Routes for listing pages, collections if items, are identified with routeSection()
. Currently the caching of data from listing pages is not supported, but should be upcoming.
/**
* Posts
*/
app.routeSection('/posts', 'content/posts/posts.dust', {});
In it's most basic form, routeSection
, and routeItem
are wrappers around the basic get
method from Express. However here we have the option to attach some additional options.
The first parameter is always the route to match, this can be a regex. The second parameter is the template that will be used to render the final response. Finally, the options
object.
There are a few optional keys, but the required one is the resource
key. This is the internal API path. This is an array, and accepts unlimited values. Each request will be processed asynchronously, and their values returned, keyed by their name.
We'll refer to this as multi-get in the future.
/**
* Posts
*/
app.routeSection('/posts', 'content/posts/posts.dust', {
resource: ['blogposts'],
processors: []
});
Another additional route option is, passQuery: true
. This forwards all query parameters from the initial request to the back-end. Additionally you may want to create more routes for preforming actions on posts.
app.routeSection('/posts', 'content/posts/posts.dust', {
resource: ['posts?range=25'],
passQuery: true
});
app.routeSection('/posts/:type', 'content/posts/posts.dust', {
resource: ['posts?filter[types]={nid}&range=25'],
passQuery: true
});
app.routeItem('/posts/:type/:title', 'content/posts/post.dust', {
resource: ['posts/{nid}']
});
/**
* Handle GET responses in a generic way
* @private
* @param {(string|regexp)} route The route to match.
* @param {string} template Template name that will be used to format the response
* @param {object} options An object that configures the Controller instance
* @return {object} res The response object
*/
var handleGet = function (route, template, options) {
/**
* Handle GET responses in a generic way
* @private
* @param {(string|regexp)} route The route to match.
* @param {string} template Template name that will be used to format the response
* @param {object} options An object that configures the Controller instance
* @return {object} res The response object
*/
app.get(route, function (req, res, next) {
var path = req.path;
path += Object.keys(req.query).length !== 0 ? helpers.serialize(req.query) : '';
debug('PATH: ' + path);
debug('ROUTE TYPE: ' + options.type);
Q(helpers.getConfig(options, config, req, template, templateEngine, keepAliveAgent))
.then(cache.get)
.then(api.get)
.then(cache.set)
.then(processData.prepare)
.then(render.parse)
.then(function (response) {
return res
.set({
'Cache-Control': 'max-age=86400',
'Expires': new Date(Date.now() + 86400000).toUTCString()
})
.send(response);
})
.fail(function (options) {
return next(options);
});
});
};
- Implemented custom methods for handling different types of requests.
routeMulti
,routeSection
,routeItem
- Each handler is an alias for
handleGet
and can attach additional options.
- Promises.
.then()
- Handlers
- Cache
- API
- Rendering
Dust is mostly just HTML
. Templates can be used for rendering in your application, and in the browser. Browser templates can be served from a CDN, or other high-performance network. Template files can also be cached in the users browser cache; all you need to send is updated JSON for rendering.
- Async & streaming operation
- Browser/node compatibility
- Extended Mustache/ctemplate syntax
- Clean, low-level API
- High performance
The following examples use some syntax you might not be familiar with, don't worry about that for now. For example, your template code,
{title}
<ul>
{#names}
<li>{name}</li>{~n}
{/names}
</ul>
and, your JSON data,
{
"title": "Famous People",
"names" : [{ "name": "Larry" },{ "name": "Curly" },{ "name": "Moe" }]
}
gives you something like,
Famous People
<ul>
<li>Larry</li>
<li>Curly</li>
<li>Moe</li>
</ul>
There is a base template at workspace/node/headless-framework/_src/templates/base_template.dust
. This should be your wrapper. If you're familiar with Sass, then the modularity of Dust will make sense.
Each of your partials should be exporting a block, but to render that block we need to let the parent partial know about it.
{+pageContent/}
The next template we'll create is the actual partial for rendering the blog posts.
{>"base_template.dust" title="Posts"/}
{<pageContent}
// your content
{/pageContent}
This let's dust know that we're returning a block named pageContent
and everything within the markup should be inserted into the body of the base template.
The syntax for loops in dust is pretty simplistic, and it's what you'll probably be using most of the time. This loops though the specific array of objects for our blog posts listing page. Within the loop, keys are available by their name, {self}
, and sub keys are available with dot-notation.
{>"base_template.dust" title="Posts"/}
{<pageContent}
{#_embedded.fk_blogposts}
{/_embedded.fk_blogposts}
{/pageContent}
The final template with some additional HTML
structure.
{>"base_template.dust" title="Posts"/}
{<pageContent}
<div class="wrapper">
<h1>posts</h1>
{#_embedded.fk_blogposts}
<h1>
<a href="{self}">
{label}
</a>
</h1>
<p> Categories:
{#categories}
{label},
{/categories}
<p>
{/_embedded.fk_blogposts}
<a href="{_links.fk_next.href}">Next</a>
</div>
{/pageContent}
You may have noticed, .then(render.parse)
. This is where the actual JSON is folded onto the template you just created. This is pretty simple, options.engine.render
is Dust. We're not dependent on Dust, as long as your template engine has render
function and returns HTML.
- Last step
- Any engine that supports
.render()
could be plugged in.- No dependencies on
DustJS
- No dependencies on
- Only return HTML.
options.engine.render(options.template, data, function (error, html) {
if (error) {
debug(error);
deferred.reject(helpers.createErrorResponse(500, [error]));
}
else {
debug(options.template);
deferred.resolve(html);
}
});