Templates Guide
Varel uses VeeMarker, a FreeMarker-compatible template engine built for V. This guide covers everything you need to render beautiful HTML views.
Table of Contents
Getting Started
Note: Template rendering imports:
import leafscale.varel.http // For Context.render()
import leafscale.varel.templates // For configuration
What is VeeMarker?
VeeMarker is a template engine that:
- Compatible with FreeMarker - Familiar syntax
- Built for V - Fast and type-safe
- Compile-time - Templates checked at build time
- Secure - Auto-escapes HTML by default
Project Structure
views/
├── layouts/
│ └── base.html.vtpl # Default layout (.html.vtpl extension required)
├── products/
│ ├── index.html.vtpl # List products
│ ├── show.html.vtpl # Show product
│ ├── new.html.vtpl # Create form
│ └── edit.html.vtpl # Edit form
├── shared/
│ ├── header.html.vtpl # Header partial (no underscore prefix)
│ ├── footer.html.vtpl # Footer partial
│ └── flash.html.vtpl # Flash messages
└── errors/
├── 404.html.vtpl # Not found
└── 500.html.vtpl # Server error
Note: Template files MUST use the .html.vtpl extension. Varel uses VeeMarker templates, and partials do not require underscore prefixes.
Rendering Templates
// controllers/products.v
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
products := models.Product.all(c.db) or { [] }
return ctx.render('products/index', {
'products': products
'page_title': 'All Products'
})
}
Template Syntax
Variables
<!-- Output variable -->
<h1>${page_title}</h1>
<!-- With default value -->
<h1>${page_title!'Untitled'}</h1>
<!-- HTML escape (automatic by default) -->
<p>${product.description?html}</p>
Conditionals
<!-- If statement -->
<#if user.is_admin>
<a href="/admin">Admin Panel</a>
</#if>
<!-- If-else -->
<#if product.stock > 0>
<button>Add to Cart</button>
<#else>
<p class="out-of-stock">Out of Stock</p>
</#if>
<!-- If-elseif-else -->
<#if order.status == 'pending'>
<span class="badge yellow">Pending</span>
<#elseif order.status == 'shipped'>
<span class="badge blue">Shipped</span>
<#elseif order.status == 'delivered'>
<span class="badge green">Delivered</span>
<#else>
<span class="badge gray">Unknown</span>
</#if>
Loops
<!-- List products -->
<#list products as product>
<div class="product">
<h3>${product.name}</h3>
<p>${product.price?string.currency}</p>
</div>
</#list>
<!-- Empty list -->
<#list products as product>
<div class="product">${product.name}</div>
<#else>
<p>No products found</p>
</#list>
<!-- Index and counters -->
<#list products as product>
<tr>
<td>${product?index + 1}</td> <!-- 1-based index -->
<td>${product.name}</td>
</tr>
</#list>
Formatting
<!-- Number formatting -->
<p>Price: ${product.price?string("0.00")}</p>
<p>Stock: ${product.stock?string.number}</p>
<!-- Date formatting -->
<p>Created: ${product.created_at?date}</p>
<p>Updated: ${product.updated_at?datetime}</p>
<!-- String operations -->
<p>${product.name?upper_case}</p>
<p>${product.name?lower_case}</p>
<p>${product.name?cap_first}</p>
<p>${product.description?truncate(100)}</p>
Important: Comparison Operators
VeeMarker only supports symbolic operators in conditionals, NOT text operators.
✅ Correct:
<#if product.stock > 0>
<button>Add to Cart</button>
</#if>
<#if user.age >= 18>
<p>Adult content visible</p>
</#if>
<#if order.status == "completed">
<span class="badge green">Done</span>
</#if>
❌ Wrong (DO NOT USE):
<#if product.stock gt 0> <!-- WILL NOT WORK -->
<#if user.age gte 18> <!-- WILL NOT WORK -->
<#if order.status eq "done"> <!-- WILL NOT WORK -->
Supported Operators:
>(greater than) - NOTgt<(less than) - NOTlt>=(greater than or equal) - NOTgte<=(less than or equal) - NOTlte==(equal) - NOTeq!=(not equal) - NOTne
Arithmetic in Conditionals
You cannot perform arithmetic directly in <#if> statements. You must use <#assign> first.
✅ Correct:
<#assign size_mb = file.size / 1024 / 1024>
<#if size_mb > 10>
<span class="warning">Large file!</span>
</#if>
❌ Wrong:
<#if (file.size / 1024 / 1024) > 10> <!-- WILL NOT WORK -->
✅ Arithmetic works in display expressions:
<!-- This works - direct display -->
<p>File size: ${(file.size / 1024 / 1024)?string("0.00")} MB</p>
<!-- But for conditionals, use assign first -->
<#assign size_mb = file.size / 1024 / 1024>
<#if size_mb > 100>
<p class="large-file">Very large file!</p>
</#if>
Common Patterns
File Size with Conditional:
<#if file.size > 0>
<span class="file-size">
${(file.size / 1024)?string("0.00")} KB
</span>
<#else>
<span class="file-size">Unknown size</span>
</#if>
Pluralization:
<p>Found ${count} item<#if count != 1>s</#if></p>
Selected Option:
<select name="category">
<#list categories as category>
<option value="${category.id}" <#if category.id == selected_id>selected</#if>>
${category.name}
</option>
</#list>
</select>
Troubleshooting VeeMarker Errors
Error: "Unexpected token in conditional"
- Cause: You used a text operator (
gt,lt, etc.) - Fix: Replace with symbolic operator (
>,<, etc.)
Error: "Cannot evaluate arithmetic in conditional"
- Cause: You tried arithmetic directly in
<#if> - Fix: Use
<#assign>to compute the value first
Error: "Unexpected parentheses in conditional"
- Cause: Complex expressions in conditionals aren't supported
- Fix: Use
<#assign>to compute intermediate values
Layouts
Default Layout
Create views/layouts/base.html.vtpl:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${page_title!'My App'}</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<#include "shared/header.html.vtpl">
<main class="container">
<#include "shared/flash.html.vtpl">
<!-- Page content goes here -->
${content}
</main>
<#include "shared/footer.html.vtpl">
<script src="/static/js/app.js"></script>
</body>
</html>
Using Layouts
// controllers/products.v
pub fn (c ProductsController) index(mut ctx http.Context) http.Response {
products := models.Product.all(c.db) or { [] }
// Render with default layout
return ctx.render('products/index', {
'products': products
'page_title': 'Products'
})
}
Custom Layouts
// Use different layout
return ctx.render_with_layout('admin/dashboard', 'layouts/admin', {
'stats': stats
'page_title': 'Admin Dashboard'
})
Partials
Creating Partials
views/shared/_header.html:
<header>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
<#if current_user??>
<a href="/dashboard">Dashboard</a>
<a href="/logout">Logout</a>
<#else>
<a href="/login">Login</a>
<a href="/signup">Sign Up</a>
</#if>
</nav>
</header>
views/shared/_flash.html:
<#if flash_success??>
<div class="alert alert-success">
${flash_success}
</div>
</#if>
<#if flash_error??>
<div class="alert alert-error">
${flash_error}
</div>
</#if>
Including Partials
<!-- Include partial -->
<#include "shared/header.html.vtpl">
<!-- Include with parameters -->
<#include "shared/product_card.html.vtpl" product=product>
Note: Include paths must specify the full .html.vtpl extension. Partial files do not use underscore prefixes.
Template Helpers
URL Helpers
<!-- Link to route -->
<a href="/products/${product.id}">View Product</a>
<!-- Link with query params -->
<a href="/products?page=${page + 1}">Next Page</a>
<!-- Form action -->
<form action="/products/${product.id}" method="POST">
<!-- form fields -->
</form>
Form Helpers
<!-- Text input -->
<input type="text" name="name" value="${product.name!''}">
<!-- Select dropdown -->
<select name="category_id">
<#list categories as category>
<option value="${category.id}"
<#if category.id == product.category_id>selected</#if>>
${category.name}
</option>
</#list>
</select>
<!-- Checkbox -->
<input type="checkbox" name="published"
<#if product.published>checked</#if>>
<!-- Radio buttons -->
<#list ['draft', 'published', 'archived'] as status>
<label>
<input type="radio" name="status" value="${status}"
<#if product.status == status>checked</#if>>
${status?cap_first}
</label>
</#list>
CSRF Token
<!-- CSRF protection for forms -->
<form action="/products" method="POST">
<!-- Get CSRF token from context -->
<input type="hidden" name="_csrf_token" value="${csrf_token}">
<input type="text" name="name">
<button type="submit">Create Product</button>
</form>
Best Practices
1. Auto-Escape HTML
VeeMarker auto-escapes HTML by default:
<!-- Safe - auto-escaped -->
<p>${product.description}</p>
<!-- Unsafe - raw HTML (only if you trust the source!) -->
<p>${product.description?no_esc}</p>
2. Provide Default Values
<!-- Good - has default -->
<h1>${page_title!'Untitled Page'}</h1>
<!-- Bad - might error if missing -->
<h1>${page_title}</h1>
3. Keep Logic in Controllers
<!-- ❌ BAD - Complex logic in template -->
<#if product.price > 100 && product.stock > 0 && !product.discontinued>
<button>Buy Now</button>
</#if>
<!-- ✅ GOOD - Logic in controller, simple check in template -->
<#if product.can_purchase>
<button>Buy Now</button>
</#if>
4. Use Partials for Reusability
<!-- ❌ BAD - Repeated code -->
<!-- products/index.html -->
<div class="product-card">...</div>
<!-- products/featured.html -->
<div class="product-card">...</div> <!-- Duplicate! -->
<!-- ✅ GOOD - Reusable partial -->
<!-- shared/_product_card.html -->
<div class="product-card">
<h3>${product.name}</h3>
<p>${product.price}</p>
</div>
<!-- products/index.html.vtpl -->
<#list products as product>
<#include "shared/product_card.html.vtpl">
</#list>
Scaffolding Views
Automatic View Generation
When you scaffold a resource with the Varel CLI, it automatically generates all views:
# Generate complete resource with views
varel generate scaffold Product name:string price:decimal description:text stock:int
# This creates:
# - views/products/index.html (list view)
# - views/products/show.html (detail view)
# - views/products/new.html (create form)
# - views/products/edit.html (edit form)
Generated Index View
views/products/index.html:
<h1>Products</h1>
<a href="/products/new">New Product</a>
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Description</th>
<th>Stock</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<#list products as product>
<tr>
<td>${product.name}</td>
<td>${product.price?string("0.00")}</td>
<td>${product.description}</td>
<td>${product.stock}</td>
<td>
<a href="/products/${product.id}">Show</a>
<a href="/products/${product.id}/edit">Edit</a>
<a href="/products/${product.id}" data-method="delete">Delete</a>
</td>
</tr>
<#else>
<tr>
<td colspan="5">No products found</td>
</tr>
</#list>
</tbody>
</table>
Generated Show View
views/products/show.html:
<h1>${product.name}</h1>
<dl>
<dt>Name:</dt>
<dd>${product.name}</dd>
<dt>Price:</dt>
<dd>$${product.price?string("0.00")}</dd>
<dt>Description:</dt>
<dd>${product.description}</dd>
<dt>Stock:</dt>
<dd>${product.stock}</dd>
</dl>
<a href="/products/${product.id}/edit">Edit</a>
<a href="/products">Back to Products</a>
Generated Form Views
views/products/new.html:
<h1>New Product</h1>
<form action="/products" method="POST">
<input type="hidden" name="_csrf_token" value="${csrf_token}">
<div>
<label for="name">Name:</label>
<input type="text" name="name" id="name" required>
</div>
<div>
<label for="price">Price:</label>
<input type="number" name="price" id="price" step="0.01" required>
</div>
<div>
<label for="description">Description:</label>
<textarea name="description" id="description"></textarea>
</div>
<div>
<label for="stock">Stock:</label>
<input type="number" name="stock" id="stock" required>
</div>
<button type="submit">Create Product</button>
<a href="/products">Cancel</a>
</form>
views/products/edit.html:
<h1>Edit Product</h1>
<form action="/products/${product.id}" method="POST">
<input type="hidden" name="_method" value="PUT">
<input type="hidden" name="_csrf_token" value="${csrf_token}">
<div>
<label for="name">Name:</label>
<input type="text" name="name" id="name" value="${product.name}" required>
</div>
<div>
<label for="price">Price:</label>
<input type="number" name="price" id="price" value="${product.price}" step="0.01" required>
</div>
<div>
<label for="description">Description:</label>
<textarea name="description" id="description">${product.description}</textarea>
</div>
<div>
<label for="stock">Stock:</label>
<input type="number" name="stock" id="stock" value="${product.stock}" required>
</div>
<button type="submit">Update Product</button>
<a href="/products/${product.id}">Cancel</a>
</form>
Customizing Generated Views
The generated views are starting points. Customize them:
<!-- Add styling -->
<div class="card">
<h1 class="card-title">${product.name}</h1>
<p class="card-text">${product.description}</p>
</div>
<!-- Add validation messages -->
<#if errors??>
<div class="alert alert-error">
<#list errors as error>
<p>${error}</p>
</#list>
</div>
</#if>
<!-- Add image uploads -->
<div>
<label for="image">Product Image:</label>
<input type="file" name="image" id="image" accept="image/*">
</div>
<!-- Add rich text editor -->
<div>
<label for="description">Description:</label>
<textarea name="description" id="description" class="rich-editor">${product.description}</textarea>
</div>
<script src="/static/js/editor.js"></script>
Summary
You've learned:
✅ VeeMarker template syntax ✅ Variables, conditionals, and loops ✅ Layouts and partials ✅ Form helpers and CSRF tokens ✅ Scaffolding views with the CLI ✅ Customizing generated views ✅ Best practices for security and maintainability
Continue to the Sessions & Auth Guide for user authentication!