The jsTACS template engine

940 words, 5-minute read

Publican uses jsTACSJavaScript Templating and Caching System – as its templating engine. Unlike other engines, there’s no special files or syntax. You just use JavaScript template literals such as:

${ data.title }

String literal expressions #

An ${ expression } must return a value to render something on the page, e.g.

${ "str" + "ing" } // string

${ 2 + 2 } // 4

${ (new Date()).getUTCFullYear() } // current year (four digits)

Strings and numbers are rendered as-is. The values:

Content data properties #

Content values for each page are available in a data object. For example, show the page title, word count, and content:

<h1>${ data.title }</h1>
<p>This article has ${ data.wordCount } words.</p>

${ data.content }

Global tacs properties #

Global values which apply throughout the site are available in a tacs object. For example, ensure links use the correct server root:

<p><a href="${ tacs.root }">home page</a></p>

The tacs value is available in publican.config.js so you can append other global values and functions. The following example sets today’s date, the site language, and a tacs.formatDate() function:

publican.config.js

import { Publican, tacs } from 'publican';

// create Publican object
const publican = new Publican();

// global date and language
tacs.today = new Date();
tacs.language = 'en-US';

// global format date function
tacs.formatDate = d => {
  return new Intl.DateTimeFormat(
    tacs.language,
    { dateStyle: 'long' }
  ).format( d )
};

// build site
await publican.build();

You can use these to show the current date in human-readable format:

<p>Last build on ${ tacs.formatDate( tacs.today ) }</p>

toArray() conversion #

The toArray() function converts any value, object, Set, or Map to an array so you can apply methods such as .map() and .filter(), e.g.

<!-- output any value -->
${ toArray( data.something ).map(m => `Value ${ m }`).join(', ') }

include() templates #

Templates can include other template files with:

${ include(<filename>) }

where <filename> is relative to the template directory. The example default.html below includes _partials/header.html:

src/template/default.html

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>${ data.title }</title>
  </head>
  <body>

    ${ include('_partials/header.html') }

    <main>
      ${ data.content }
    </main>

  </body>
</html>

_partials/header.html includes _partials/nav.html:

src/template/_partials/header.html

<header>
  ${ include('_partials/nav.html') }
  <h1>${ data.title }</h1>
</header>

_partials/nav.html has no further includes:

src/template/_partials/nav.html

<nav><a href="${ tacs.root }">Home</a><nav>

Template literals in markdown #

You can use template literals in markdown content but some care may be necessary to avoid problems with the HTML conversion. Simple expressions will often work as expected:

<!-- this should work -->
## ${ data.title }

However, expressions inside or between code blocks may not execute and more complex expressions break the markdown to HTML parser.

<!-- this will fail -->
${ data.all.map(i => i.title) }

You can work around these issues in a number of ways:

  1. Use a double-bracket ${{ expression }}. This denotes a real expression irrespective of where it resides in the markdown (they are stripped from the content before HTML conversion).

  2. Use HTML snippets in your markdown file, e.g.

    A markdown paragraph.
    
    <p>This ${ "HTML block" } is skipped by the markdown parser.</p>
    
  3. Simplify expressions using custom jsTACS functions.

  4. Only use complex expressions in HTML content or template files. These are not processed by the markdown parser.

Runtime expressions #

!{ expression } identifies expressions that are ignored during the build but converted to ${ expression } at the end and remain in the rendered file. Publican can therefore create sites that are mostly static, with islands of dynamic values rendered at runtime.

Consider the following content:

src/content/index.md

---
title: Home page
---

My home page.

It uses the default template:

src/template/default.html

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>${ data.title }</title>
  </head>
  <body>

    <main>
      <h1>${ data.title }</h1>
      ${ data.content }
    </main>

    <footer>
      <p>Today's date is !{ data.today }</p>
    </footer>

  </body>
</html>

Publican builds the following static HTML page. The title and content have rendered, but !{ data.today } has become ${ data.today }:

build/index.html

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Home page</title>
  </head>
  <body>

    <main>
      <h1>Home page</h1>
      <p>My home page.</p>
    </main>

    <footer>
      <p>Today's date is ${ data.today }</p>
    </footer>

  </body>
</html>

This partially-rendered template can be used in a framework such as Express.js with jsTACS as its rendering engine. It can dynamically set the data.today property on every page visit:

import express from 'express';
import { templateEngine } from 'jstacs';

const
  app = express(),
  port = 8181;

app.engine('html', templateEngine);
app.set('views', './build/');
app.set('view engine', 'html');

// render template at ./templates/index.html
app.get('/', (req, res) => {
  res.render('index', {
    today: (new Date()).toUTCString()
  });
});

app.listen(port, () => {
  console.log(`Express started on port ${port}`);
});