Render a template to a string with hashJS.render, with no DOM and no browser involved.
The same template syntax, called as an ordinary function. This is what makes the library usable on a server: a template is a string, the data is an object, and the result is HTML you can write straight into a response.
<style>
body { font: 14px/1.5 system-ui, sans-serif; margin: 0; color: #24292f; }
h4 { margin: 0 0 6px; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: #57606a; }
pre { background: #0d1117; color: #c9d1d9; padding: 10px 12px; border-radius: 6px;
overflow-x: auto; font-size: 12px; line-height: 1.45; margin: 0 0 16px; }
ul { list-style: none; padding: 0; margin: 0; border: 1px solid #d8dee4; border-radius: 6px; }
li { display: flex; justify-content: space-between; padding: 7px 12px; border-bottom: 1px solid #eaeef2; }
li:last-child { border-bottom: 0; }
a { color: #0969da; text-decoration: none; }
.price { font-variant-numeric: tabular-nums; color: #57606a; }
</style>
<h4>The string the server produces</h4>
<pre id="source"></pre>
<h4>The same string, in a browser</h4>
<div id="rendered"></div>
<script src="https://cdn.jsdelivr.net/gh/richdafunk/hashJS@v1.3.6/hashJS.js"></script>
<script>
// A template is just a string, and hashJS.render is just a function.
// There is no DOM in this call and nothing to bind to — it takes a
// template and data and hands back finished HTML.
const template = [
'<ul>',
'#for(let p of products) {#',
' <li>',
' <a href="/product/#p.sku#">#htmlEncode(p.name)#</a>',
' <span class="price">#p.price# NOK</span>',
' </li>',
'#}#',
'</ul>'
].join('\n');
const data = {
products: [
{ sku: 'A-1042', name: 'Ceramic mug', price: 249 },
{ sku: 'B-3310', name: 'Notebook, A5 "dotted"', price: 129 },
{ sku: 'C-0087', name: 'Pencil set <HB & 2B>', price: 89 }
]
};
const html = hashJS.render(template, data);
// Left as text so you can read exactly what came out.
document.getElementById('source').textContent = html.trim();
// And again as markup, which is what a browser receives from the server.
document.getElementById('rendered').innerHTML = html;
</script> // server.js — the same template and the same call, in a request handler.
//
// Nothing about the template changes when it moves to a server. There is no
// DOM to bind to and no element to resolve, so hashJS.render is the whole API.
const http = require('http');
// The library is a single file with no dependencies, so it is vendored rather
// than installed. It exports through module.exports as well as to the global.
const hashJS = require('./hashJS.js');
// Compile once, at startup. Compilation is the expensive half of rendering,
// and a page template does not change between requests.
const page = hashJS.compile(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>#htmlEncode(title)#</title>
</head>
<body>
<h1>#htmlEncode(title)#</h1>
<ul>
#for(let p of products) {#
<li>
<a href="/product/#p.sku#">#htmlEncode(p.name)#</a>
<span>#p.price# NOK</span>
</li>
#}#
</ul>
#if(products.length === 0) {#
<p>Nothing in this category yet.</p>
#}#
</body>
</html>`);
http.createServer(async function (req, res) {
const products = await db.products.inCategory(req.url);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(page({ title: 'Catalogue', products: products }));
}).listen(3000); The constructor form needs a document: it resolves an element, reads its markup and writes the result back into the page. hashJS.render skips all of that and does only the part that matters — compile the template, run it against the data, return the string.
Note #htmlEncode(p.name)#. Values are substituted exactly as they are, so anything originating with a user has to be escaped where it is written. One product name here contains < and & and another contains quotes; both come out as text rather than as markup.
The server listing uses hashJS.compile instead. Compilation is the expensive half of rendering and a page template does not change between requests, so it belongs at startup rather than in the handler.