Awesome
<em>µ</em>content
<sup>Social Media Photo by Bonnie Kittle on Unsplash</sup>
📣 Community Announcement
Please ask questions in the dedicated forum to help the community around this project grow ♥
A <em>micro</em> SSR oriented HTML/SVG content generator, but if you are looking for a <em>micro</em> FE content generator, check µhtml out.
const {render, html} = require('ucontent');
const fs = require('fs');
const stream = fs.createWriteStream('test.html');
stream.once('open', () => {
render(
stream,
html`<h1>It's ${new Date}!</h1>`
).end();
});
V2 Breaking Change
The recently introduced data
helper could conflict with some node such as <object>
, hence it has been replaced by the .dataset
utility. Since element.dataset = object
is an invalid operation, the sugar to simplify data-
attributes is now never ambiguous and future-proof: <element .dataset=${...} />
it is.
This is aligned with µhtml and lighterhtml recent changes too.
API
- a
render(writable, what)
utility, to render in aresponse
orstream
object, viawritable.write(content)
, or through a callback, the content provided by one of the tags. The function returns the result ofcallback(content)
invoke, or the the passed first parameter as is (i.e. theresponse
or thestream
). Please note this helper is not mandatory to render content, as any content is an instance ofString
, so that if you prefer to render it manually, you can always use directlycontent.toString()
instead, as every tag returns a specialized instance of String. This API doesn't set any explicit headers forresponse
objects based onwhat
. - a
html
tag, to render HTML content. Each interpolation passed as layout content, can be either a result fromhtml
,css
,js
,svg
, orraw
tag, as well as primitives, such asstring
,boolean
,number
, or evennull
orundefined
. The result is a specialized instance ofString
with a.min()
method to produce eventually minified HTML content via html-minifier. All layout content, if not specialized, will be safely escaped, while attributes will always be escaped to avoid layout malfunctions. - a
svg
tag, identical to thehtml
one, except minification would preserve any self-closing tag, as in<rect />
. - a
css
tag, to create CSS content. Its interpolations will be stringified, and it returns a specialized instance ofString
with a.min()
method to produce eventually minified CSS content via csso. If passed ashtml
orsvg
tag interpolation content,.min()
will be automatically invoked. - a
js
tag, to create JS content. Its interpolations will be stringified, and it returns a specialized instance ofString
with a.min()
method to produce eventually minified JS content via terser. If passed ashtml
orsvg
tag interpolation content,.min()
will be automatically invoked. - a
raw
tag, to pass along interpolated HTML or SVG values any kind of content, even partial one, or a broken, layout.
Both html
and svg
supports µhtml utilities but exclusively for feature parity <sup><sub>(html.for(...)
and html.node
are simply aliases for the html
function)</sub></sup>.
Except for html
and svg
tags, all other tags can be used as regular functions, as long as the passed value is a string, or a specialized instance.
This allow content to be retrieved a part and then be used as is within these tags.
import {readFileSync} from 'fs';
const code = js(readFileSync('./code.js'));
const style = css(readFileSync('./style.css'));
const partial = raw(readFileSync('./partial.html'));
const head = title => html`
<head>
<title>${title}</title>
<style>${style}</style>
<script>${code}</script>
</head>
`;
const body = () => html`<body>${partial}</body>`;
const page = title => html`
<!doctype html>
<html>
${head(title)}
${body()}
</html>
`;
All pre-generated content can be passed along, automatically avoiding minification of the same content per each request.
// will be re-used and minified only once
const jsContent = js`/* same JS code to serve */`;
const cssContent = css`/* same CSS content to serve */`;
require('http')
.createServer((request, response) => {
response.writeHead(200, {'content-type': 'text/html;charset=utf-8'});
render(response, html`
<!doctype html>
<html>
<head>
<title>µcontent</title>
<style>${cssContent}</style>
<script>${jsContent}</script>
</head>
</html>
`.min()).end();
})
.listen(8080);
If one of the HTML interpolations is null
or undefined
, an empty string will be placed instead.
Note: When writing to
stream
objects using therender()
API make sure to call end on it
Production: HTML + SVG Implicit Minification
While both utilities expose a .min()
helper, repeated minification of big chunks of layout can be quite expensive.
As the template literal is the key to map updates, which happen before .min()
gets invoked, it is necessary to tell upfront if such template should be minified or not, so that reusing the same template later on, would result into a pre-minified set of chunks.
In order to do so, html
and svg
expose a minified
boolean property, which is false
by default, but it can be switched to true
in production.
import {render, html, svg} from 'ucontent';
// enable pre minified chunks
const {PRODUCTION} = process.env;
html.minified = !!PRODUCTION;
svg.minified = !!PRODUCTION;
const page = () => html`
<!doctype html>
<html>
<h1>
This will always be minified
</h1>
<p>
${Date.now()} + ${Math.random()}
</p>
</html>
`;
// note, no .min() necessary
render(response, page()).end();
In this way, local tests would have a clean layout, while production code will always be minified, where each template literal will be minified once, instead of each time .min()
is invoked.
Attributes Logic
- as it is for µhtml too, sparse attributes are not supported: this is ok
attr=${value}
, but this is wrong:attr="${x} and ${y}"
. - all attributes are safely escaped by default.
- if an attribute value is
null
orundefined
, the attribute won't show up in the layout. aria=${object}
attributes are assigned hyphenized asaria-a11y
attributes. Therole
is passed instead asrole=...
.style=${css...}
attributes are minified, if the interpolation value is passed ascss
tag..dataset=${object}
setter is assigned hyphenized asdata-user-land
attributes..contentEditable=${...}
,.disabled=${...}
and any attribute defined as setter, will not be in the layout if the passed value isnull
,undefined
, orfalse
, it will be in the layout if the passed value istrue
, it will contain escaped value in other cases. The attribute is normalized without the dot prefix, and lower-cased.on...=${'...'}
events passed as string or passed asjs
tag will be preserved, and in thejs
tag case, minified.on...=${...}
events that pass a callback will be ignored, as it's impossible to bring scope in the layout.
Benchmark
Directly from pelo project but without listeners, as these are mostly useless for SSR.
Rendering a simple view 10,000 times:
node test/pelo.js
tag | time (ms) |
---|---|
ucontent | 117.668ms |
pelo | 129.332ms |
How To Live Test
Create a test.js
file in any folder you like, then npm i ucontent
in that very same folder.
Write the following in the test.js
file and save it:
const {render, html} = require('ucontent');
require('http').createServer((req, res) => {
res.writeHead(200, {'content-type': 'text/html;charset=utf-8'});
render(res, html`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ucontent</title>
</head>
<body>${html`
<h1>Hello There</h1>
<p>
Thank you for trying µcontent at ${new Date()}
</p>
`}</body>
</html>
`)
.end();
}).listen(8080);
You can now node test.js
and reach localhost:8080, to see the page layout generated.
If you'd like to test the minified version of that output, invoke .min()
after the closing </html>
template tag:
render(res, html`
<!DOCTYPE html>
<html lang="en">
...
</html>
`.min()
).end();
You can also use html.minified = true
on top, and see similar results.
API Summary Example
import {render, css, js, html, raw} from 'ucontent';
// turn on implicit html minification (production)
html.minified = true;
// optionally
// svg.minified = true;
render(content => response.end(content), html`
<!doctype html>
<html lang=${user.lang}>
<head>
<!-- dynamic interpolations -->
${meta.map(({name, content}) =>
html`<meta name=${name} content=${content}>`)}
<!-- explicit CSS minification -->
<style>
${css`
body {
font-family: sans-serif;
}
`}
</style>
<!-- explicit JS minification -->
<script>
${js`
function passedThrough(event) {
console.log(event);
}
`}
</script>
</head>
<!-- discarded callback events -->
<body onclick=${() => ignored()}>
<div
class=${classes.join(' ')}
always=${'escaped'}
.contentEditable=${false}
.dataset=${{name: userName, id: userId}}
aria=${{role: 'button', labelledby: 'id'}}
onmouseover=${'passedThrough.call(this,event)'}
>
Hello ${userName}!
${raw`<some> valid, or even ${'broken'}, </content>`}
</div>
</body>
</html>
`);