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:
- Addition: +
- Subtraction: -
- Division: /
- Division remainder: %
- Multiplication: *
- Power: **
{{1 + 2}}
{{6 - 10}}
{{5 / 4}}
{{29 % 8}}
{{7 * 9}}
{{3 ** myNumber}}
Comparisons
- =
- ==
- ===
- !=
- !==
- >
- >=
- <
- <=
{{#if var * 2 >= 7}}True!{{/if}}
Logic
- && (and)
- || (or)
- ! (not)
- Use parentheses to group expressions
{{#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}}
The documentation is clear and provides good examples, but I'd suggest a few improvements in structure and content. Here's how I'd rewrite it to include the @key
feature and enhance readability:
each
The #each
tag allows you to iterate over arrays and objects. Within the loop, you can access:
this
to reference the current item@index
to get the current iteration index (0-based)@key
to get the current key (for objects) or index (for arrays)
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:
{{this}}
- The current item{{@index}}
- Current iteration index (0-based){{@key}}
- Current key (for objects) or index (for arrays){{propertyName}}
- Direct property access{{object.property}}
- Dot notation{{object["property"]}}
- Bracket notation
include
Renders an included HTML template within another template.
This tag loads the specified file, replaces its placeholders with the provided data, and returns the rendered HTML to be included in the main template.
It's particularly useful for embedding common sections, such as headers, footers, or navigation menus, that can be reused across multiple templates.
By separating sections into individual files, you can promote the DRY (Don't Repeat Yourself) principle.
This works because the include
tag injects data from the parent template's data object into the child template. In other words, included templates can access parent template variables not explicitly passed to them.
Given the route handler:
// Using default views directory
const app = new LiteNode()
// Or with custom template directory
const app = new LiteNode("public", "templates")
app.get("/", (req, res) => {
const content = "Some content"
res.render("index.html", {
docTitle: "Templating",
docDescription: "Rendering HTML templates using STE in LiteNode",
title: "Templating in LiteNode with STE",
content,
footerText: `Copyright ${new Date().getFullYear()} - LebCit`,
})
})
Assume we have in the "views" or "templates" directory a "components" folder with 3 files:
- head.html
<head>
<meta name="description" content="{{docDescription}}" />
<title>{{docTitle}}</title>
<link rel="stylesheet" href="/static/css/styles.css" />
</head>
- header.html
<header>
<title>{{title}}</title>
</header>
- footer.html
<footer>
<p>{{footerText}}</p>
<script src="/static/js/scripts.js"></script>
</footer>
The index.html file will use the include
tag to insert each component where it should be placed:
<!DOCTYPE html>
<html>
{{#include("components/head.html")}}
<body>
{{#include("components/header.html")}}
<div>{{content}}</div>
{{#include("components/footer.html")}}
</body>
</html>
And the generated output will be:
<html>
<head>
<meta name="description" content="Rendering HTML templates using STE in LiteNode" />
<title>Templating</title>
<link rel="stylesheet" href="/static/css/styles.css" />
</head>
<body>
<header>
<title>Templating in LiteNode with STE</title>
</header>
<div>Some content</div>
<footer>
<p>Copyright 2025 - LebCit</p>
<script src="/static/js/scripts.js"></script>
</footer>
</body>
</html>
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:
- Transform data once and use it multiple times
- Break down complex data processing into more manageable steps
- Improve template readability by separating data transformation from presentation
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:
template
: Path to the HTML template file (relative to the views directory)data
: Object containing values for template variables
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:
template
: Path to the HTML template filedata
: Object containing values for template variablesoutputPath
: Destination path for the generated HTML file
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
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
orpartials
folder for reusable components
- Keep templates in a dedicated directory (default:
Security
- Only use
html_
prefix for trusted content - Sanitize user-generated content
- Validate all inputs before rendering
- Only use
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