The jsTACS template engine

1,100 words, 6-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 HTML conversion.

Double bracket ${{ expressions }} #

Simpler expressions will work as expected, but you can use double-bracket ${{ expressions }} and !{{ expressions }} when necessary. These denote real expressions irrespective of where they reside in the markdown.

Use double brackets in code blocks when an expression must be parsed rather than rendered as syntax:

markdown code block

```js
// render expression as code:
// console.log( '${ data.title }' );

   console.log( '${ data.title }' );

// parse expression:
// console.log( 'This Page Heading' );

   console.log( '${{ data.title }}' );

```

Single line expressions #

An expression on a single line such as:

markdown source

${ data.title }

is rendered inside an HTML paragraph tag:

HTML output

<p>This Page's Title</p>

If you need a different tag, you can use an HTML snippet, e.g.

markdown source

<div>${ data.title }</div>

Or you can use HTML comments when no tags are required. These are ignored by the browser and removed during minification.

markdown source

<!-- -->${ data.title }<!-- -->

HTML entities #

You can use HTML entities to avoid expression parsing:

Further options #

If you still encounter problems, you can:

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

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

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

Runtime expressions #

!{ expression } identifies an expression that is 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 (also using jsTACS).

Consider the following content:

src/content/index.md

---
title: Home page
---

My home page.

It uses a 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}`);
});