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}}
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
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
- Flexible Path Resolution: Supports both absolute and relative paths from any directory level
- Contextual Path Handling: Resolves paths based on the including template's location
- Unlimited Nesting: Works at any directory depth
- Data Inheritance: Included templates have access to parent template data
- Project Structure Freedom: No restrictions on file placement or naming conventions
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:
- Standard Mode (with a base directory):
const app = new LiteNode() // → "views"
const app = new LiteNode("static", "templates") // → "templates"
- 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
- Organize by Feature: Group related templates together in feature-specific directories
- Use Relative Paths: When templates are related, use relative paths to maintain portability
- Shared Components: Place shared components in a common directory that's easily accessible
- Clear Naming: Use clear, consistent naming conventions for your directories and files
- 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:
- 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