/ucontent

An SSR HTML content generator.

Primary LanguageJavaScript

µcontent

Build Status Coverage Status

sunflowers

Social Media Photo by Bonnie Kittle on Unsplash

A micro SSR oriented HTML/SVG content generator, but if you are looking for a micro 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 a response or stream object, via writable.write(content), or through a callback, the content provided by one of the tags. The function returns the result of callback(content) invoke, or the the passed first parameter as is (i.e. the response or the stream). Please note this helper is not mandatory to render content, as any content is an instance of String, so that if you prefer to render it manually, you can always use directly content.toString() instead, as every tag returns a specialized instance of String. This API doesn't set any explicit headers for response objects based on what.
  • a html tag, to render HTML content. Each interpolation passed as layout content, can be either a result from html, css, js, svg, or raw tag, as well as primitives, such as string, boolean, number, or even null or undefined. The result is a specialized instance of String 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 the html 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 of String with a .min() method to produce eventually minified CSS content via csso. If passed as html or svg 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 of String with a .min() method to produce eventually minified JS content via terser. If passed as html or svg 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 (html.for(...) and html.node are simply aliases for the html function).

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 the render() 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 or undefined, the attribute won't show up in the layout.
  • aria=${object} attributes are assigned hyphenized as aria-a11y attributes. The role is passed instead as role=....
  • style=${css...} attributes are minified, if the interpolation value is passed as css tag.
  • .dataset=${object} setter is assigned hyphenized as data-user-land attributes.
  • .contentEditable=${...}, .disabled=${...} and any attribute defined as setter, will not be in the layout if the passed value is null, undefined, or false, it will be in the layout if the passed value is true, 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 as js tag will be preserved, and in the js 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>
`);