Templating
LiteNode includes STE (Simple Template Engine) — an AST-based engine for rendering .html template files with dynamic data.
Directory Setup
By default, STE looks for templates in the views directory. Override it when initializing LiteNode:
const app = new LiteNode() // views/
const app = new LiteNode("public", "templates") // templates/
Reference files relative to the template root — no need to include the root directory name:
res.render("index.html") // views/index.html
res.render("components/header.html") // views/components/header.html
res.render("admin/dashboard/index.html")
.html files. Any other extension will cause an error.
Variables
Pass a data object to render(). Booleans, strings, numbers, arrays, and objects are all supported.
res.render("index.html", {
isHome: true,
name: "LebCit",
fruits: ["cherry", "kiwi", "peach"],
user: { country: "Lebanon", "is-admin": true },
})
Access in the template using {{ }}:
{{isHome}} {{name}} {{fruits[2]}} {{user.country}} {{user["is-admin"]}}
Both dot notation (user.country) and bracket notation (user["is-admin"]) work. Bracket notation is required when keys contain spaces or special characters.
Expressions
Arithmetic
{{1 + 2}}
{{6 - 10}}
{{5 / 4}}
{{29 % 8}}
{{7 * 9}}
{{3 ** myNumber}}
Comparisons
Supported operators: =, ==, ===, !=, !==, >, >=, <, <=
{{#if var * 2 >= 7}}True!{{/if}}
Logic
Operators: &&, ||, ! — use parentheses to group.
{{#if !(((2 + 3) * numbers[1]) < 1) || (numbers[0] - 6) > -10}}True!{{/if}}
Ternary
{{condition ? "True" : "False"}}
{{a ? "value1" : b ? "value2" : "default"}}
Filters
Apply transformations to values using the pipe operator:
{{ value | filterName }}
{{ value | filterName(arg1, arg2) }}
See the STE Filters page for the full filter reference.
Tags
if
{{#if condition}}
<!-- shown when true -->
{{/if}}
With elseif and else, and nesting:
{{#if userRole == "admin"}}
<p>Admin Panel</p>
{{#if hasFullAccess}}
<p>Full access.</p>
{{#else}}
<p>Limited access.</p>
{{/if}}
{{#elseif userRole == "editor"}}
<p>Editor Dashboard</p>
{{#else}}
<p>User Dashboard</p>
{{/if}}
not
{{#not isLoggedIn}}
<p>Please log in to continue.</p>
{{/not}}
each
Iterates over arrays and objects. Inside the loop:
{{this}}— current item{{@index}}— 0-based index{{@key}}— current key (for objects) or index (for arrays)
Array:
{{#each fruits}}
<p>{{@index}}: {{this}}</p>
{{/each}}
Object:
{{#each person}}
<p>{{@key}}: {{this}}</p>
{{/each}}
Array of objects:
{{#each users}}
<p>#{{@index}} — {{name}} from {{address.city}}</p>
{{/each}}
Nested loops — append a number to avoid conflicts:
{{#each categories}}
<li>{{name}}
<ul>
{{#each1 items}}
<li>{{name}}</li>
{{/each1}}
</ul>
</li>
{{/each}}
With #include:
{{#each posts}}
{{#include("./post-card.html")}}
{{/each}}
include
Renders another template file inline. Included templates inherit the parent's data context.
Absolute path (from the template root):
{{#include("components/header.html")}}
{{#include("layouts/base.html")}}
Relative path (from the current template's location):
{{#include("./header.html")}}
{{#include("../shared/footer.html")}}
{{#include("../../layouts/base.html")}}
Basic layout pattern:
<!-- views/index.html -->
<!DOCTYPE html>
<html>
{{#include("components/head.html")}}
<body>
{{#include("components/header.html")}}
<main>{{content}}</main>
{{#include("components/footer.html")}}
</body>
</html>
Edge Case — Root Mode
When using root mode (new LiteNode("public", "./")), relative paths in nested includes can lose context. Prefer absolute paths to avoid this:
<!-- ✅ Safe in root mode -->
{{#include("components/header.html")}}
<!-- ⚠️ May fail in nested includes with root mode -->
{{#include("../components/header.html")}}
set
Creates or modifies variables directly in the template. Supports booleans, strings, arrays, objects, and complex nested structures.
{{#set greeting = "Hello"}}
{{#set isDev = true}}
{{#set tags = ["html", "css", "js"]}}
{{greeting}}, developer! Favourite: {{tags[2]}}
Complex data:
{{#set config = {
title: "My Site",
nav: ["Home", "About", "Blog"]
}}}
{{config.title}} — {{config.nav[1]}}
With filters:
{{#set articles = [...] }}
{{#set grouped = articles | groupBy("category")}}
{{#each grouped.Programming}}
<article>{{title}}</article>
{{/each}}
Raw HTML
Variables prefixed with html_ bypass template evaluation and inject raw HTML directly. Useful for pre-rendered content like parsed Markdown.
res.render("post.html", {
html_content: "<p>Hello <strong>World</strong></p>",
})
<!-- In post.html — rendered as-is, not escaped -->
{{html_content}}
You can also reassign html_ variables with set:
{{#set html_content = "<p>Overridden!</p>"}}
{{html_content}}
STE API
res.render()
res.render(template: string, data?: object): Promise<void>
Renders a template and sends it as the HTTP response.
app.get("/profile/:id", async (req, res) => {
const user = await db.getUser(req.params.id)
res.render("profile.html", {
user,
html_avatar: `<img src="${user.avatar}" alt="Avatar">`,
isOwner: req.user?.id === user.id,
})
})
app.renderToFile()
app.renderToFile(template: string, data: object, outputPath: string): Promise<void>
Renders a template to a static HTML file on disk. Used for static site generation (SSG).
await app.renderToFile("index.html", { title: "Home" }, "_site/index.html")
See the Examples page for a complete SSG build script.
Best Practices
- Organise templates in subdirectories:
components/,layouts/,pages/ - Use
html_prefix only for trusted, pre-sanitized content - Use
renderToFile()to pre-build static pages that don't need dynamic data - Break large templates into smaller included components