I think the answer is more programming, but I'll show you what I do. I use custom middleware that let's me combine arbitrary transformative processes before I get to my final HTML document output. So, for example, I have the following filters in my middleware.js module, which I will explain in turn.
So simple views just use normal jade with its various filters for markdown, javascript, coffeescript. Some views, for example a blog post, require a more sophisticated middleware chain, which goes like this.
First, based on the request, I establish the file that holds the core content for this response, and set that as a property on res.viewPath. This could be a raw HTML fragment file or a markdown file. Then I send the response through a series of middleware transformations. I use res.html and res.dom to store intermediate representations of the response as it is being built up.
This one just stores raw HTML (just a document body fragment with no head or layout).
html = function(req, res, next) {
if (!/\.html$/.test(res.viewPath)) return next();
return fs.readFile(res.viewPath, "utf8", function(error, htmlText) {
res.html = htmlText;
return next(error);
});
};
This one will convert a markdown file to HTML (using the markdown-js module).
markdownToHTML = function(req, res, next) {
if (!/\.md$/.test(res.viewPath)) return next();
return fs.readFile(res.viewPath, "utf8", function(error, markdownText) {
res.html = markdown(markdownText);
return next(error);
});
};
I have a sub-layout that goes within my master layout but around each blog post. So I wrap the blog post in the sublayout here. (Separate code not shown generates the res.post object from a json metadata file).
blogArticle = function(req, res, next) {
var footerPath, post;
post = res.post;
footerPath = path.join(__dirname, "..", "templates", "blog_layout.jade");
return fs.readFile(footerPath, "utf8", function(error, jadeText) {
var footerFunc;
if (error) return next(error);
footerFunc = jade.compile(jadeText);
res.html = footerFunc({
post: post,
body: res.html
});
return next();
});
};
Now I wrap my layout around the main content HTML. Note that I can set things like the page title here, or wait until later since I can manipulate the response via jsdom after this. I do body: res.html || "" so I can render an empty layout and insert the body later if that is more convenient.
exports.layout = function(req, res, next) {
var layoutPath;
layoutPath = path.join(__dirname, "..", "templates", "layout.jade");
return fs.readFile(layoutPath, "utf8", function(error, jadeText) {
var layoutFunc, locals;
layoutFunc = jade.compile(jadeText, {
filename: layoutPath
});
locals = {
config: config,
title: "",
body: res.html || ""
};
res.html = layoutFunc(locals);
return next(error);
});
};
Here's where the really powerful stuff comes. I convert the HTML string into a jsdom document object model which allows for jQuery based transformations on the server side. The toMarkup function below just allows me to get the HTML back without the extra <script> tag for our in-memory jquery which jsdom has added.
exports.domify = function(req, res, next) {
return jsdom.env(res.html, [jqueryPath], function(error, dom) {
if (error) return next(error);
res.dom = dom;
dom.toMarkup = function() {
this.window.$("script").last().remove();
return this.window.document.doctype + this.window.document.innerHTML;
};
return next(error);
});
};
So here's a custom transformation I do. This can replace a made-up DSL tag like <flickrshow href="http://flickr.com/example"/> with real valid HTML, which otherwise would be a big nasty <object> boilerplate I would have to duplicate in each blog post, and if flickr ever changed the boilerplate markup they use, it would be a maintenance pain to go fix it in many individual blog post markdown files. The boilerplate they currently use is in the flickrshowTemplate variable and contains a little mustache placeholders {URLs}.
exports.flickr = function(req, res, next) {
var $ = res.dom.window.$;
$("flickrshow").each(function(index, elem) {
var $elem, URLs;
$elem = $(elem);
URLs = $elem.attr("href");
return $elem.replaceWith(flickrshowTemplate.replace(/\{URLs\}/g, URLs));
});
return next();
};
Ditto for embedding a youtube video. <youtube href="http://youtube.com/example"/>.
exports.youtube = function(req, res, next) {
var $ = res.dom.window.$;
$("youtube").each(function(index, elem) {
var $elem, URL;
$elem = $(elem);
URL = $elem.attr("href");
return $elem.replaceWith(youtubeTemplate.replace(/\{URL\}/, URL));
});
return next();
};
Now I can change the title if I want, add/remove javascripts or stylesheets, etc. Here I set the title after the layout has already been rendered.
postTitle = function(req, res, next) {
var $;
$ = res.dom.window.$;
$("title").text(res.post.title + " | Peter Lyons");
return next();
};
OK, time to go back to final HTML.
exports.undomify = function(req, res, next) {
res.html = res.dom.toMarkup();
return next();
};
Now we ship it off!
exports.send = function(req, res) {
return res.send(res.html);
};
To tie it all together in order and have express use it, we do
postMiddleware = [
loadPost,
html,
markdownToHTML,
blogArticle,
layout,
domify,
postTitle,
flickr,
youtube,
undomify,
send
]
app.get("/your/uri", postMiddleware);
Concise? No. Clean? I think so. Flexible? Extremely. Blazingly fast? Probably not wicked fast as I believe jsdom is one of the more heavyweight things you can do, but I'm using this as a static site generator so speed is irrelevant. Of course, it would be trivial to add another function to the begining and end of the middleware chain to write the final HTML to a static file and serve it directly if it is newer than the corresponding markdown page body content file. Stackoverflowers, I'd love to hear thoughts and suggestions on this approach!