GitHub GitHub
docs / Templating

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")
Information STE only renders .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