LiteNode's logo

LiteNode

Docs GitHub logo
▶ Usage

Documentation

Templating

LiteNode features an integrated, powerful AST-based template engine, called STE (Simple Template Engine), for rendering HTML files.
This engine allows you to render .html files located in your templates directory (default: views) or in any sub-folders within it.
This setup provides a straightforward way to organize and manage your HTML templates, making it easy to maintain and structure your application's views.

Files Directory

When initializing LiteNode, you can specify a custom directory for your templates:

// Use default directories (static="static", views="views")
const app = new LiteNode()

// Or specify custom directories
const app = new LiteNode("public", "templates")

By default, STE renders HTML files from the views directory or its sub-folders. However, you can configure a different directory when initializing LiteNode. There's no need to include the template directory name in the file path. If the file is directly in your template directory, simply use the filename. If the file is in a subfolder, like "components" you would reference it as "components/a-file.html".

Examples

// Using default views directory
const app = new LiteNode()

// index.html is directly in the "views" directory
app.get("/", async (req, res) => {
    res.render("index.html")
})

// Using custom templates directory
const app = new LiteNode("public", "templates")

// index.html is directly in the "templates" directory
app.get("/", async (req, res) => {
    res.render("index.html")
})

// index.html is in a "components" subfolder of the "templates" directory
app.get("/", async (req, res) => {
    res.render("components/index.html")
})

// index.html is in templates → hook → head
app.get("/", async (req, res) => {
    res.render("hook/head/index.html")
})

Template Extension

STE only supports and expects template files with the .html extension. Any attempt to render a template with a different file extension will result in an error, clearly indicating that only HTML files are allowed.

Variables

When rendering a template, you can pass a data object containing various types of data—such as booleans, strings, arrays, and objects—that can be used within the template. For example:

app.get("/", async (req, res) => {
    res.render("index.html", {
        isIndexRoute: true,
        name: "LebCit",
        fruits: ["cherry", "kiwi", "peach"],
        user: { country: "Lebanon", "is-admin": true },
    })
})

In the index.html file located directly in the "views" directory, you can access and display these variables as follows:

{{isIndexRoute}}
<br />
{{name}}
<br />
{{fruits}}
<br />
{{fruits[2]}}
<br />
{{user.country}}
<br />
{{user["is-admin"]}}

Note: Ensure the variable names in the data object match the case used in the template, as JavaScript is case-sensitive. Both dot notation (user.country) and bracket notation (user["country"]) are valid for accessing object properties.

Bracket notation is particularly useful when the object key contains spaces, special characters, or starts with a number, which would otherwise not be valid in dot notation. In these cases, the key name must be quoted in both the data object and the template.

Expressions

As mentioned earlier, STE allows you to pass booleans, strings, arrays, and objects from the data object into a template. You can also use the set tag to create or modify these variables directly within the template.

Arithmetic

STE allows you to operate on values. The following operators are available:

{{1 + 2}}
{{6 - 10}}
{{5 / 4}}
{{29 % 8}}
{{7 * 9}}
{{3 ** myNumber}}

Comparisons

{{#if var * 2 >= 7}}True!{{/if}}

Logic

{{#not var * 2 >= 7 && myNum / 4 <= 3}}True!{{/not}}
{{#if !(((2 + 3) * numbers[1]) < 1) || (numbers[0] - 6) > -10}}True!{{/if}}

Ternary

STE doesn't have a dedicated ternary tag because it already supports if, elseif, else, and not. However, you can still use a ternary operator, just like in JavaScript, without the need for a specific tag, as shown below:

{{condition ? "True" : "False"}}
{{condition1 ? "value1" : "condition2" ? "value2" : "default"}}

Filters

STE ships filters that can be applied to variables. They are called with a pipe operator (|) and can take arguments.

{{ value | filterName }}
{{ value | filterName(arg1, arg2) }}

To better understand and utilize the built-in filters in STE, please refer to the dedicated filters page in the documentation by following this link: Filters Documentation.

Tags

Tags are special blocks that perform operations on sections of the template.

if

Evaluates a condition and includes the content if the condition is true.

{{#if condition}}
    <!-- Content to include if condition is true -->
    <!-- Otherwise nothing will be displayed -->
{{/if}}

The condition can be a simple variable and/or a complex arithmetical logic to be evaluated like:

{{#if ( (numOne + 5) * 2 > 50 && (numTwo - 5) < 25 || 10 * 3 = numThree) }}
    <!-- Content to include if the following conditions are true -->
{{/if}}

You can use if with elseif and else just like in JavaScript and nest them if you want:

{{#if userRole == 'admin'}}
    <p>Admin Panel</p>
    {{#if hasFullAccess}}
        <p>You have full access to all features.</p>
    {{#else}}
        <p>You have limited access to certain features.</p>
    {{/if}}
{{#elseif userRole == 'editor'}}
    <p>Editor Dashboard</p>
    {{#if hasPublishingPermission}}
        <p>You can publish content.</p>
    {{#else}}
        <p>You can only edit drafts.</p>
    {{/if}}
{{#else}}
    <p>User Dashboard</p>
    {{#if hasPremiumAccess}}
        <p>You have premium features enabled.</p>
    {{#else}}
        <p>You are using the basic version.</p>
    {{/if}}
{{/if}}

not

Evaluates a condition and includes the content if the condition is false.

{{#not isLoggedIn}}
    <p>
        Please log in to continue.
    </p>
{{/not}}

each

The #each tag allows you to iterate over arrays and objects. Within the loop, you can access:

Array

{{#each fruits}}
    <p>
        {{@index}}: {{this}}
    </p>
{{/each}}

Object

{{#each person}}
    <p>
        {{@key}}: {{this}}
    </p>
{{/each}}

<!-- Example output for {name: "John", age: 30}:
    <p>name: John</p>
    <p>age: 30</p>
-->

Array of Objects

Access object properties using dot notation or bracket notation:

{{#each users}}
    <p>
        #{{@index}} - {{name}}
        Address: {{address.city}}, {{address["zip_code"]}}
    </p>
{{/each}}

Nested Loops

For nested iterations, append a number to the each tag (each1, each2, etc.):

{{#each categories}}
    <li>
        Category {{@index}}: {{name}}
        <ul>
            {{#each1 items}}
                <li>
                    Item {{@index}}: {{name}}
                </li>
            {{/each1}}
        </ul>
    </li>
{{/each}}

Access Reference

Within an #each block, you can use:

include

The include tag is a powerful feature for rendering HTML templates within other templates. It provides flexible path resolution capabilities that adapt to your project's structure, allowing you to organize templates in any way that suits your needs.

Key Features

Path Resolution Modes

Absolute Paths (from base directory)
// In any template
{{#include("components/header.html")}}
{{#include("layouts/default.html")}}

The engine looks for these files directly in your base directory (default: views).

Relative Paths (from current template)
// Reference files in same directory
{{#include("./header.html")}}

// Go up one directory
{{#include("../shared/footer.html")}}

// Go up multiple directories
{{#include("../../layouts/base.html")}}

Directory Structure Examples

The include system works seamlessly with any directory structure:

│── views/                      # Standard base directory
│   ├── index.html             # Root level template
│   ├── about.html
│   ├── components/            # Single level nesting
│   │   ├── header.html
│   │   └── footer.html
│   ├── admin/                 # Deep nesting example
│   │   ├── dashboard/
│   │   │   ├── index.html
│   │   │   └── components/
│   │   │       └── sidebar.html
│   │   └── shared/
│   │       └── nav.html
│   └── themes/                # Another deep structure
│       └── default/
│           └── layouts/
│               └── base.html

Usage Examples

Here are some practical examples showing different ways to organize and include templates:

Basic Component Include
<!-- views/index.html -->
<!DOCTYPE html>
<html>
    {{#include("components/head.html")}}
    <body>
        {{#include("components/header.html")}}
        <main>{{content}}</main>
        {{#include("components/footer.html")}}
    </body>
</html>
Nested Layout Pattern
<!-- views/themes/default/layouts/base.html -->
<!DOCTYPE html>
<html>
    {{#include("../partials/head.html")}}
    <body>
        {{content}}
    </body>
</html>

<!-- views/admin/dashboard/index.html -->
{{#include("../../themes/default/layouts/base.html")}}
    {{#include("./components/sidebar.html")}}
    <div class="dashboard">{{dashboardContent}}</div>
Shared Components
<!-- views/admin/dashboard/index.html -->
<div class="admin-page">
    {{#include("../shared/nav.html")}}
    {{#include("./components/dashboard-content.html")}}
</div>

Data Context

All included templates inherit the data context from their parent template. For example:

app.get("/dashboard", (req, res) => {
    res.render("admin/dashboard/index.html", {
        user: {
            name: "John Doe",
            role: "Admin",
        },
        pageTitle: "Dashboard",
        stats: {
            visitors: 1000,
            sales: 50,
        },
    })
})

Any included template can access this data:

<!-- views/admin/shared/nav.html -->
<nav>
    <p>Welcome, {{user.name}}</p>
    <span>Role: {{user.role}}</span>
</nav>

<!-- views/admin/dashboard/components/dashboard-content.html -->
<div class="stats">
    <h2>{{pageTitle}} Overview</h2>
    <p>Visitors: {{stats.visitors}}</p>
    <p>Sales: {{stats.sales}}</p>
</div>

Operation Modes

The include system works in two modes:

  1. Standard Mode (with a base directory):
const app = new LiteNode() // → "views"
const app = new LiteNode("static", "templates") // → "templates"
  1. Root Mode (with project directory as base):
const app = new LiteNode("static", "./")

Both modes support the same path resolution features, but root mode allows access to templates anywhere in your project structure.

Best Practices

  1. Organize by Feature: Group related templates together in feature-specific directories
  2. Use Relative Paths: When templates are related, use relative paths to maintain portability
  3. Shared Components: Place shared components in a common directory that's easily accessible
  4. Clear Naming: Use clear, consistent naming conventions for your directories and files
  5. Maintain Hierarchy: Structure your templates to reflect their relationships and dependencies

The enhanced include system gives you complete freedom in organizing your templates while maintaining consistent behavior across your entire project structure.

Edge Cases and Limitations

When using root mode ("./"), there's an important edge case to be aware of when working with relative paths in nested includes. Consider this directory structure:

├── themes/
│   ├── default/
│   │   ├── layouts/
│   │   │   └── index.html
│   │   └── components/
│   │       ├── header.html
│   │       └── header-headings.html

And these templates:

<!-- themes/default/layouts/index.html -->
<!DOCTYPE html>
<html lang="en">
    {{#include("../components/header.html")}}
</html>

<!-- themes/default/components/header.html -->
<h2>This is the header</h2>
{{#include("./header-headings.html")}}

When initialized with root mode:

const app = new LiteNode("public", "./")
app.get("/", (req, res) => {
    res.render("themes/default/layouts/index.html")
})

This will fail because the relative path ../components/header.html loses the correct context in nested includes. The system will incorrectly look for files in themes/components/ instead of themes/default/components/.

Solution: When using root mode, prefer absolute paths from your theme root to avoid path resolution issues:

<!-- themes/default/layouts/index.html -->
<!DOCTYPE html>
<html lang="en">
    {{#include("components/header.html")}}
</html>

This absolute path approach will work consistently in all cases when using root mode.

set

The set directive allows you to create new variables or modify existing ones directly within templates. Variables created with set are accessible throughout the template after their declaration. You can set booleans, strings, arrays, objects, and complex data structures such as arrays of objects.

Basic

A simple example demonstrating different data types:

{{#set myName = "LebCit"}}
{{#set isDev = true}}
{{#set loves = ["HTML", "CSS", "JS"]}}
{{#if isDev}}
    {{myName}} loves {{loves[2]}}
{{/if}}

Complex

The set directive can handle deeply nested objects and arrays with complex property names:

{{#set complexData = {
    "data.items": [
        { "item.name": "First", "item.value": 100 },
        { "item.name": "Second", "item.value": 200 }
    ],
    nested: {
        arrays: [
            [1, 2, 3],
            [4, 5, 6]
        ],
        "deep.property": {
            "very.deep": "Found me!"
        }
    },
    mixedArray: [
        {
            "prop.name": "Test",
            items: [
                { "sub.item": "A" },
                { "sub.item": "B" }
            ]
        }
    ]
}}}

{{complexData["data.items"][0]["item.name"]}}
{{complexData.nested["deep.property"]["very.deep"]}}
{{complexData.mixedArray[0]["prop.name"]}}
{{complexData.mixedArray[0].items[1]["sub.item"]}}

Filters

The set directive can be combined with filters to transform data as it's being assigned. This powerful feature allows you to process data and store the results for later use:

{{#set articles = [
    { id: 1, title: "Getting Started with JavaScript", category: "Programming" },
    { id: 2, title: "Understanding CSS Grid", category: "Web Design" },
    { id: 3, title: "Node.js Best Practices", category: "Backend" },
    { id: 4, title: "The Power of TypeScript", category: "Programming" },
    { id: 5, title: "Responsive Design Tips", category: "Web Design" }
]}}

{{#set groupedArticles = articles | groupBy("category")}}

In this example, groupedArticles will contain the articles grouped by their category, resulting in an object where each key is a category and its value is an array of matching articles. The filtered result is stored in the variable and can be used throughout the template:

{{#each groupedArticles.Programming}}
    <article>
        {{title}}
    </article>
{{/each}}

This technique is particularly useful when you need to:

Raw HTML

You can use the html_ property to inject raw HTML content directly into a template. When the html_ identifier is encountered, STE bypasses content evaluation and replaces the html_X placeholder with its raw content.
This is useful when you need to inject HTML directly into templates without escaping special characters.

Given the route handler:

app.get("/custom-html", (req, res) => {
    const customHtml = "<p>The if tag is used like so: {{#if 2 < 1 }} True {{/if}}</p>"
    res.render("custom.html", { html_content: customHtml })
})

Now we can use our html_ identifier in custom.html like so:

{{html_content}}

STE will not evaluate the content of html_content and will return the paragraph:

The if tag is used like so: {{#if 2 < 1 }} True {{/if}}

You can even modify the content of an html_ identifier with set:

Raw HTML before: {{html_content}}
{{#set html_content = "<p>The set tag is awesome!</p>"}}
Raw HTML after: {{html_content}}

The returned output will be:

Raw HTML before: The if tag is used like so: {{#if 2 < 1 }} True {{/if}}
Raw HTML after: The set tag is awesome!

STE API

STE provides two main methods for rendering templates: render for HTTP responses and renderToFile for static file generation.

res.render

res.render(template: string, data: object): Promise<void>

Renders an HTML template and sends it as the HTTP response.

Parameters:

Static Data Example:

app.get("/welcome", (req, res) => {
    res.render("welcome.html", {
        title: "Welcome",
        message: "Hello World",
        html_content: "<p>This is <strong>HTML</strong> content</p>",
    })
})

Dynamic Data Example:

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="Profile picture">`,
        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.

Parameters:

Static Data Example:

// Assuming a route handler for index page
app.get("/", async (req, res) => {
    res.render("index.html", { title: "Home" })
})

// We can Generate a static version of the index page as follows
async function generateStaticIndex() {
    await app.renderToFile("index.html", { title: "Home" }, "_site/index.html")
}

generateStaticIndex()

Dynamic Data Example:

app.get("/blog/:slug", async (req, res) => {
    const post = await db.getPost(req.params.slug)
    res.render("blog/post.html", { post })
})

async function generateStaticBlog() {
    const posts = await db.getAllPosts()
    for (const post of posts) {
        await app.renderToFile("blog/post.html", { post }, `_site/blog/${post.slug}.html`)
    }
}

generateStaticBlog()

Best Practices

  1. Template Organization

    • Keep templates in a dedicated directory (default: views)
    • Consider using a descriptive name for your template directory (e.g., templates, pages, views)
    • Use subdirectories for different sections of your site
    • Create a components or partials folder for reusable components
  2. Security

    • Only use html_ prefix for trusted content
    • Sanitize user-generated content
    • Validate all inputs before rendering
  3. Performance

    • Use static generation for pages that don't need dynamic content
    • Break down large templates into smaller, reusable parts
    • Include only necessary data in the template context

Content